JavaScript 作为一种典型的多范式编程语言,这两年随着React的火热,函数式编程的概念也开始流行起来,RxJS、cycleJS、lodashJS、underscoreJS、ramda等多种开源库都使用了函数式的特性。记录一下相关的概念和知识。
纯函数
对于相同的输入,永远得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。
比如 slice 和 splice,这两个函数的作用并无二致——但是注意,它们各自的方式却大不同,但不管怎么说作用还是一样的。我们说 slice 符合纯函数的定义是因为对相同的输入它保证能返回相同的输出。而 splice 却会嚼烂调用它的那个数组,然后再吐出来;这就会产生可观察到的副作用,即这个数组永久地改变了。
追求“纯”的理由
1.可缓存性
纯函数总能够根据输入来做缓存,实现缓存的一种典型技术就是memoize技术:
2.可移植性/自文档化
纯函数完全是自给自足的,依赖很明确,因此更易于观察和理解。
3.可测试性
纯函数让测试更加容易。我们不需要伪造一个“真实的”支付网关,或者每一次测试之前都要配置、之后都要断言状态(assert the state)。只需简单地给函数一个输入,然后断言输出就好了。
4.合理性
如果一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换的,那么我们就说这段代码是引用透明的。
由于纯函数总是能够根据相同的输入返回相同的输出,所以它们就能够保证总是返回同一个结果,这也就保证了引用透明性。
5.并行代码
最后一点,也是决定性的一点:我们可以并行运行任意纯函数。因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因副作用而进入竞争态。
并行代码在服务端 js 环境以及使用了 web worker 的浏览器那里是非常容易实现的,因为它们使用了线程。不过出于对非纯函数复杂度的考虑,当前主流观点还是避免使用这种并行。
函数柯里化
传递给函数的一部分参数来调用它,让它返回一个函数去处理剩下的参数。
事实上柯里化是一种“预加载”函数的方法,通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的编写函数的方法。
柯里化的实现
函数组合
纯函数以及把它柯里化写出的洋葱代码h(g(f(x))),为了解决函数嵌套问题,我们需要用到“函数组合”。

函数就像数据的管道(pipe)。那么,函数合成就是将这些管道连了起来,让数据一口气从多个管道中穿过。
Point Free
函数无须提及将要操作的数据是什么。
声明式与命令式代码
命令式代码的意思就是,我们通过编写一条又一条指令去让计算机执行一些动作,这其中一般都会涉及到很多繁杂的细节。而声明式就要优雅很多了,我们可以通过写表达式的方式来声明我们想要干嘛,而不是一步一步的指示。
惰性求值、惰性函数
|
|
高阶函数
函数当参数,把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象。
这是个高阶函数的例子,既把函数当做参数传递,又让函数执行后返回了另外一个函数。
容器
|
|
使用Container.of作为构造器,就不用到处去写糟糕的new关键字了。
functor
functor 是实现了map函数并遵守一些特定规则的容器类型。
上面代码中,Container是个函子,它的map方法接受函数f作为参数,然后返回一个新的函子,里面包含的值是被f处理过的(f(this.__value))
Maybe 函子
函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个空值(比如null),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错。
Either 函子
条件运算if…else是最常见的运算之一,函数式编程里面,使用either函子表达。Either函子内部有两个值:左值(Left)和右值(right)。右值是征程情况下使用的值,左值是右值不存在时使用的值。
Left和Right唯一的区别在于map方法的实现,Right.map的行为和我们之前提到的map函数一样。但是Left.map就很不同了,它不会对容器做任何事情,只是很简单地把这个容器拿进来又扔出去,这个特性就意味着,left可以用来传递一个错误信息。
IO 函子
|
|
IO 跟之前的 functor 不同的地方在于,它的 __value 总是一个函数。不过我们不把它当作一个函数——实现的细节我们最好先不管。IO 把非纯执行动作捕获到包裹函数里,目的是延迟执行这个非纯动作。就这一点而言,我们认为 IO 包含的是被包裹的执行动作的返回值,而不是包裹函数本身。这在 of 函数里很明显:IO(function(){ return x }) 仅仅是为了延迟执行,其实我们得到的是 IO(x)。
Monad 函子
Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤,你只要提供下一步运算所需的函数,整个运算就会自动进行下去。Monad让我们避开了嵌套地狱,可以轻松地进行深度嵌套的函数式编程,比如IO和其他异步任务。
Monad 函子的作用是,总是返回一个单层的函子,他有一个flatMap方法,与map方法作用相同,唯一的区别就是如果生成了一个嵌套函子,他会取出后者内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的问题。
如果函数f返回的是一个函子,那么this.map(f)就会生成一个嵌套的函子。所以,join方法保证了flatMap方法总是返回一个单层的函子。这意味着嵌套的函子会被铺平。