最近在看JavaScript函数式编程的部分,有些阅读之后的心得和理解,我想总结并分享出来。
首先是什么是函数式编程?函数式编程实际上与过程式编程一样,是一种编程的风格。函数式编程是以函数作为第一公民。
提到函数式,必须理解下面几个概念:
1. 纯函数
纯函数是指一个输出对应一个输入,且没有任何副作用的函数
如果理解没有任何副作用呢?就是和函数外部没有任何联系。像IO操作,文件读取这一类就不是纯函数。
var fs = require('fs');
function readFile(filename) {
fs.readFile(filename, function(err, data) {
//...
});
}
像这里的readFile函数就不是一个纯函数,因为里面涉及文件读取操作。
那么纯函数有什么用呢?函数式编程与纯函数又有什么关联?
这时候我们需要了解范畴学的概念。
2. 范畴学
范畴是数学上的一个概念,通俗点来讲,范畴是一个集合,是由许多个值以及它们之间的关联所组成,其中每个值可以通过关联变化成另外一个值。将这个知识迁移到函数式编程,这个关联就是函数。
所以函数式编程实际上是由一个值,通过函数来变化为另一个值。是一个求值的运算,那就意味着函数必须是纯函数,不能有其他的副作用,否则就不符合函数式编程的原则了。
了解了范畴学之后,我们再来接触一下柯里化。
3. 柯里化
什么是柯里化?就是将一部分参数传递给函数进行调用,并返回一个函数处理剩下的参数。
举个简单的例子:
function add(x, y) {
return x + y;
}
改为柯里化的写法,就是:
function add(x) {
return function(y) {
return x + y;
}
}
那么为什么要柯里化,这样写有什么好处?这里先留个悬念,等后面讲完组合我再解释。
4. 组合(compose)
在函数式编程里,组合是很重要的一个概念,它可以将多个不同功能的纯函数给结合起来,实现一种类似管道的功能。
比如,
function g(x) {
//...
}
function f(x) {
//...
}
f(g(a));
a要经过g函数和f函数的处理才能得到最终的值,那么我们可以像上面那样写,而在函数式编程中,通过一个compose将他们结合起来。
function compose(f, g) {
return function(x) {
return f(g(x));
}
};
介绍完compose,就可以解答为什么函数式编程需要柯里化了。因为compose中的函数是处理单参数,像f(x)、g(x),如果是f(a, b, c)和g(y, z)就不能这样处理了。所以需要用柯里化将函数转换为单参数函数。
讲完了基本的概念,下面来介绍一个叫函子(Functor)的东西
函子是用来将一个范畴转换到另一个范畴。
比如原本有一个姓名的范畴集合,通过将某个转换函数传入函子,将它转换成各自最喜欢科目的一个范畴。
下面来介绍函子的两个常用的功能:
1. map
这个操作可以将一个范畴的值转换到另一个范畴中,同时返回这个新容器。
晒上代码:
class Functor {
constructor(v) {
this.__value = v;
}
map(f) {
return new Functor(f(this.__value));
}
}
2. of
创建一个新容器的时候一般用new进行构造调用,可是new是来自面向对象的编程,不适用于函数式编程,所以有了of方法。
Functor.of = function(v) {
return new Functor(v);
};
接下来介绍不同种类的函子:
1. Maybe函子
Maybe函子是为了解决可能传入空值的情况。
class Maybe extends Functor {
map(f) {
return this.__value ? Maybe.of(f(this.__value)) : Maybe.of(null);
}
}
这样可以应用在以下的情况
Maybe.of(null).map(function(x) {
console.log(x);
});
2. Either函子
有两个值Right和Left,Right是正常情况下用的值,而Left是Right不存在时采用的默认值
class Either extends Functor {
constructor(l, r) {
super();
this.left = l;
this.right = r;
}
map(f) {
return this.right ?
Either.of(this.left, f(this.right)) :
Either.of(f(this.left), this.right);
}
}
3. ap函子
函子里面包含的值,也有可能是函数,不一定是数值。这时候就用到ap
class Ap extends Functor {
ap(F) {
return Ap.of(this.__value(F.value));
}
}
用ap函子,可以实现对多参数的函数进行链式调用
例如:
function add(x) {
return function(y) {
return x + y;
}
}
Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
4. Monad函子
最后,函子之后可能会出现函子,变成了多层的嵌套。这也是合法的。
如果按照之前的操作,当出现嵌套的时候取值要一直调用this.__value,而Monad提供了一个叫flatMap的方法,可以保证在
嵌套的情况下,永远取出最里面的值。将这个嵌套的函子铺平。
class Monad extends Functor {
join() {
return this.val;
}
flatMap(f) {
return this.map(f).join();
}
}
join
方法保证了flatMap
方法总是返回一个单层的函子
参考文章:
1. 函数式编程入门教程 ---- 阮一峰
2. JS 函数式编程指南