深入函数式编程(FP)

深入函数式编程(FP)

Tags
函数式编程
JavaScript
HOC
闭包
偏函数
CreatedTime
Aug 22, 2022 07:39 AM
Slug
2021-09-01-fp
UpdatedTime
Last updated August 22, 2022

FP的要求

在函数式编程语言世界里面:
  • function必须是一等公民。一等公民是指function可以作为一个function参数,也可以作为function返回值,也可以赋值给变量或者其他对象属性。
    • JavaScript中,function就是一等公民。
  • 引用透明。同样的in params,不论多少次,返回必须要一样,没有造成副作用(side effect),函数是纯函数(pure function)。
    • 什么是Side Effect?修改传入的参数、外部状态、发送http请求、db查询、打印log、获取input、dom查询、访问(系统)状态等等
  • 将简单的指令式调用封装为函数。例如👇
function formatter(formatFn) { return function inner(str){ return formatFn( str ); }; } var lower = formatter( function (v){ return v.toLowerCase(); } );

FP的优势

  • 低复杂度:function不会有状态,也不会直接存取或读取外接状态。对于相同输入,一定会有相同输出。
  • 无需语句(statement):所有Pure Functional Programming Languages 都是由表达式(expression) 所组成的,这跟其他大多数语言不同,大多数程式语言由表达式(expression) 和语句(statement) 组成。

Pure Function

纯函数是指没有副作用的函数。在js中,最常见的是如果将对象传给参数,那么函数拿到的是对象的引用,内部就可能会修改外部数据状态,函数不再pure了。
可以使用不可变数据来避免这种情况,可以参考
ImmutableJS 实战
、ES6语法中的let/const与Object.freeze()/Object.isFrozen()

Higher-order Function

高阶函数是至少满足下列一个条件的函数:
  • 接受一个或多个函数作为输入
  • 输出一个函数

Closure

闭包定义 当一个函数可以记住并存取到不同scope(作用域)的变量,甚至这个函数在不同scope被执行,称之Closure
举个例子:
function greaterThan(n) { return function inner (m) { return m > n; }; } var greaterThan10 = greaterThan(10); console.log(greaterThan10(11)); // true
n的scopre是greaterThan。greaterThan运行之后,按理说n应该被回收。但是由于inner中用到了n,所以它被“暂时保留”了下来。这个称之为:n被内部函数inner closure

Partial Application

偏函数应用定义:partial application是指一种减少函数参数个数Arity的过程。
什么 Arity指的是形式参数parameter的个数。js中可以通过func.length获取到必填参数的个数。
偏函数工具函数实现:
function partial(fn, ...presetArgs) { return function partiallyApplied(...laterArgs) { return fn( ...presetArgs, ...laterArgs ) } }
可以利用偏函数,基于基础函数(参数多,更灵活),二次封装函数(参数少,针对某种场景)。例如:
function ajax( url, data, cb ) { } // ajax 异步请求 let getOrder = partial(ajax, "http://some.api/order") // 用于请求order接口的异步函数 let getLastOrder = partial(getOrder, { id: ORDER_ID }) // 用于获取最后一个order的异步函数 let getLastOrder2 = partial(ajax, "http://some.api/order", { id: ORDER_ID }) // 第二种定义方法
偏函数用途:
  • 切割传参数的时空背景(时间、程式的不同区块),原本的方式需要在调用的时候立刻传入所有的参数,如果你的函数中有些参数待会才传入,可以考虑使用curryingpartial application
  • 实现柯里化(见下一部分)
  • 可以隐藏细节,增强可读性(例如上面基于ajax的封装)
参考文章:

Point-free style

Point-free(又写成Pointfree,中文:无参数,无点),正式名称为:tacit programming,其中的point(点)指的就是函数的parameter(形式参数)。
作用:Pointfree透过隐藏parameter - argument形参-实参对应),减少视觉上的干扰,上层操作不直接操作数据,只合成运算过程。
举个例子:
function double(x) { return x * 2 } [1,2,3,4,5].map( double ) // point free style [1,2,3,4,5].map( v => double(v) ) // not point free style

Functor 函子

定义 A functor is something that can be mapped over. 来自 HaskellFantasy Land specification
那something 是什么?就是一组值放在某个容器(集合)里,容器就是指这些值怎么摆放,比如说阵列(依序列)或者物件(用key 来取值...等)。
那什么叫that can be mapped over?也就是js中map(..) 做的事,把每个值经过mapper(..) 得到新值,最后再把新值依照放进同样结构的容器后return 。
JS的Array对象上,使用Map还是forEach呢? map(..) 是用来映射值的,不是来产生副作用的。如果要传入带有副作用的函数,建议还是使用 forEach(..) 或者干脆写 loop 避免造成困惑。
总结来说,任何具有map方法(映射关系)的资料结构,都可以视为functor。例如:
class Wrapper { constructor (value) { this.value = value } map (f) { return new Wrapper(f(this.value)) } } // 使用代码 let something = new Wrapper(2) // something => { value: 2 } let otherthing = something.map(function (value) { return value + 3; }) // otherthing => { value: 5 }
class Wrapper 就可以视作一个 functor,因为它具有map方法。而且在map方法中,通过传入的mapper function,使内部值转化,最后返回映射后的新值。

Continuation-passing style(CPS)

在JS中,continuation表示函数结束后下一步骤的callback,也就是接着要做的事,而CPS就是在函数结束后把要做的事指定给下一个函数(当作参数)。
我理解是这种回调callback风格就是cps。
代码参考:
"use strict"; var sumRecursion = (function IIFE(){ return function sum(...nums) { return recursion(nums, v=>v) } function recursion([result, ...nums], cont) { if (nums.length == 0) return cont(result) return recursion( nums, function(v) { return cont(result + v) }) } })(); console.log(sumRecursion( 3, 1, 17, 94, 8 ));
可以参考:

参数处理

过滤参数

场合:将两个函数组合,比如说把A function 传入B function ,但此时B function 传入的参数跟A function 数量不符合。比如说:
  • B:map(...)传3个变数value, indexarray
  • A:parseInt(...)接收两个变数stringradix
例如:
['1', '2'].map(parseInt) // 返回:[1, NaN] // 因为'2'传进来相当于执行:parsetInt('2', 1)。1进制肯定是不可能出现'2'
封装unary函数,仅让一个参数能通过:
function unary(fn) { return function onlyOneArg(arg){ return fn( arg ); }; }
使用效果:
['1', '2'].map(unary(parseInt)) // parseint经过包装后,只会接收到第1个参数 // 返回:[1, 2]

传1返1

基础函数:接受一个参数,然后原封不动返回
function identical(v) { return v }
这样某些场景下,就不用写() =>v 的语法,例如:
str.split().filter(identical) // 过滤空串 promise1.then(identical(val)).then(func1)

条件判断

取反函数:
function not (testerfn) { return function negated (...args) { return !testerfn(...args) } }
条件函数:
function when (testerfn, fn) { return function conditional (...args) { if (testerfn(...args)) { return fn(...args) } } }

参考文章