函数式编程 3
# 函数式编程 3
函数式编程的理解:
- 与面向对象编程(Object-oriented programming)和过程式编程(Procedural programming)并列的编程范式。
- 最主要的特征是,函数是第一等公民。
- 强调将计算过程分解成可复用的函数,典型例子就是 map 方法和 reduce 方法组合而成 MapReduce 算法。
- 只有纯的、没有副作用的函数,才是合格的函数。
# 一、范畴论
理解函数式编程的关键,就是理解范畴论。
也就是说,彼此之间存在某种关系的概念、事物、对象等等,都构成"范畴",随便什么东西,只要能找出它们之间的关系,就能定义一个"范畴"。
# 1.1 态射和变形
上图中,各个点与它们之间的箭头,就构成一个范畴。箭头叫态射,各个点就是变形
箭头表示范畴成员之间的关系,正式的名称叫做"态射"(morphism)。范畴论认为,同一个范畴的所有成员,就是不同状态的"变形"(transformation)。
通过"态射",一个成员可以变形成另一个成员。
# 1.2 范畴与容器
我们可以把"范畴"想象成是一个容器,里面包含两样东西。
- 值(value)
- 值的变形关系,也就是函数。
class Category {
constructor(val) {
this.val = val;
}
addOne(x) {
return x + 1;
}
}
2
3
4
5
6
7
8
9
函数中 Category
就是容器,里面包含一个值 this.val
和一种变形关系 addOne
这里的范畴,就是所有彼此之间相差 1 的数字。
# 1.3 范畴论与函数式编程的关系
范畴论使用函数,表达范畴之间的关系。
伴随着范畴论的发展,就发展出一整套函数的运算方法。这套方法起初只用于数学运算,后来有人将它在计算机上实现了,就变成了今天的"函数式编程"。
本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。
所以,你明白了吗,为什么函数式编程要求函数必须是纯的,不能有副作用?因为它是一种数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则了。
总之,在函数式编程中,函数就是一个管道(pipe)。这头进去一个值,那头就会出来一个新的值,没有其他作用。
# 二、函数的合成与柯里化
# 函数的合成
如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做"函数的合成"(compose)。
上图中,X 和 Y 之间的变形关系是函数 f,Y 和 Z 之间的变形关系是函数 g,那么 X 和 Z 之间的关系,就是 g 和 f 的合成函数 g·f。
const compose = function(f, g) {
return function(x) {
return f(g(x));
};
};
// 函数的合成还必须满足结合律。
compose(f, compose(g, h));
// 等同于
compose(compose(f, g), h);
// 等同于
compose(f, g, h);
// 例如:
var toUpperCase = function(x) {
return x.toUpperCase();
};
var exclaim = function(x) {
return x + "!";
};
var shout = compose(exclaim, toUpperCase);
shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 柯里化
柯里化就是把一个多参数的函数,转化为单参数函数
// 柯里化之前
function add(x, y) {
return x + y;
}
add(1, 2); // 3
// 柯里化之后
function addX(y) {
return function(x) {
return x + y;
};
}
addX(2)(1); // 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
有了柯里化以后,我们就能做到,所有函数只接受一个参数
# 三、函子(Functor)
函数不仅可以用于同一个范畴之中值的转换,还可以用于将一个范畴转成另一个范畴
# 函子的概念
函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。
它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。
# 函子的代码实现
class Functor {
constructor(val) {
this.val = val;
}
map(f) {
return new Functor(f(this.val));
}
}
2
3
4
5
6
7
8
9
上面代码中,Functor 是一个函子,它的 map 方法接受函数 f 作为参数,然后返回一个新的函子,里面包含的值是被 f 处理过的(f(this.val))。
💖 一般约定,函子的标志就是容器具有 map 方法。该方法将容器里面的每一个值,映射到另一个容器。学习函数式编程,实际上就是学习函子的各种运算
# 示例:
new Functor(2).map(function(two) {
return two + 2;
});
// Functor(4)
new Functor("flamethrowers").map(function(s) {
return s.toUpperCase();
});
// Functor('FLAMETHROWERS')
new Functor("bombs").map(_.concat(" away")).map(_.prop("length"));
// Functor(10)
2
3
4
5
6
7
8
9
10
11
12
# 四、of 方法
函数式编程一般约定,函子有一个 of 方法,用来生成新的容器。
下面就用 of 方法替换掉 new。
Functor.of = function(val) {
return new Functor(val);
};
然后,前面的例子就可以改成下面这样。
Functor.of(2).map(function (two) {
return two + 2;
});
// Functor(4)
2
3
4
5
6
7
8
9
10
# 五、 Maybe 函子
函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个空值(比如 null),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错。
Functor.of(null).map(function(s) {
return s.toUpperCase();
});
// TypeError
2
3
4
上面代码中,函子里面的值是 null,结果小写变成大写的时候就出错了。
Maybe 函子就是为了解决这一类问题而设计的。简单说,它的 map 方法里面设置了空值检查。
# 六、Either 函子
条件运算 if...else 是最常见的运算之一,函数式编程里面,使用 Either 函子表达。
Either 函子内部有两个值:左值(Left)和右值(Right)。右值是正常情况下使用的值,左值是右值不存在时使用的默认值。
class Either extends Functor {
constructor(left, right) {
this.left = left;
this.right = right;
}
map(f) {
return this.right
? Either.of(this.left, f(this.right))
: Either.of(f(this.left), this.right);
}
}
Either.of = function(left, right) {
return new Either(left, right);
};
// 下面是用法。
var addOne = function(x) {
return x + 1;
};
Either.of(5, 6).map(addOne);
// Either(5, 7);
Either.of(1, null).map(addOne);
// Either(2, null);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Either 函子的常见用途是提供默认值。下面是一个例子。
Either.of({ address: "xxx" }, currentUser.address).map(updateField);
// 上面代码中,如果用户没有提供地址,Either 函子就会使用左值的默认地址。
2
Either 函子的另一个用途是代替 try...catch,使用左值表示错误。
# 七、ap 函子
函子里面包含的值,完全可能是函数。我们可以想象这样一种情况,一个函子的值是数值,另一个函子的值是函数。
# 八、Monad 函子
Monad 函子的重要应用,就是实现 I/O (输入输出)操作
I/O 是不纯的操作,普通的函数式编程没法做,这时就需要把 IO 操作写成 Monad 函子,通过它来完成