函数式编程

函数式编程入门教程

为什么要『函数式编程』?

范畴论

函数式编程的起源,是一门叫做范畴论(Category Theory)的数学分支,它认为世界上所有的概念体系,都可以抽象成一个个的”范畴”(category)。

范畴的概念

彼此之间存在某种关系的概念、事物、对象等等,都构成”范畴”。随便什么东西,只要能找出它们之间的关系,就能定义一个”范畴”。

范畴论认为,同一个范畴的所有成员,就是不同状态的”变形”(transformation)。通过”态射”,一个成员可以变形成另一个成员。

数学模型

  • 所有成员是一个集合
  • 变形关系是函数

也就是说,范畴论是集合论更上层的抽象,简单的理解就是”集合 + 函数”。

理论上通过函数,就可以从范畴的一个成员,算出其他所有成员。

范畴与容器

我们可以把”范畴”想象成是一个容器,里面包含两样东西。

  • 值(value)
  • 值的变形关系,也就是函数。

下面我们使用代码,定义一个简单的范畴。

1
2
3
4
5
6
7
8
9
class Category {
constructor(val) {
this.val = val;
}

addOne(x) {
return x + 1;
}
}

上面代码中,Category是一个类,也是一个容器,里面包含一个值(this.val)和一种变形关系(addOne)。你可能已经看出来了,这里的范畴,就是所有彼此之间相差1的数字。

范畴论与函数式编程的关系

伴随着范畴论的发展,就发展出一整套函数的运算方法。这套方法起初只用于数学运算,后来有人将它在计算机上实现了,就变成了今天的”函数式编程”。

在函数式编程中,函数就是一个管道(pipe)。这头进去一个值,那头就会出来一个新的值,没有其他作用。

函数的合成与柯里化

函数式编程有两个最基本的运算:合成和柯里化。

函数的合成

如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做”函数的合成”(compose)。

函数的合成必须满足结合律。

1
2
3
4
5
compose(f, compose(g, h))
// 等同于
compose(compose(f, g), h)
// 等同于
compose(f, g, h)

柯里化

f(x)g(x)合成为f(g(x)),有一个隐藏的前提,就是fg都只能接受一个参数。如果可以接受多个参数,比如f(x, y)g(a, b, c),函数合成就非常麻烦。

这时就需要函数柯里化了。所谓”柯里化”,就是把一个多参数的函数,转化为单参数函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 柯里化之前
function add(x, y) {
return x + y;
}

add(1, 2) // 3

// 柯里化之后
function addX(y) {
return function (x) {
return x + y;
};
}

addX(2)(1) // 3

有了柯里化以后,我们就能做到,所有函数只接受一个参数。

函子

函数不仅可以用于同一个范畴之中值的转换,还可以用于将一个范畴转成另一个范畴。这就涉及到了函子(Functor)。

函子的概念

函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。

它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。

函子的代码实现

任何具有map方法的数据结构,都可以当作函子的实现。

1
2
3
4
5
6
7
8
9
class Functor {
constructor(val) {
this.val = val;
}

map(f) {
return new Functor(f(this.val));
}
}

上面代码中,Functor是一个函子,它的map方法接受函数f作为参数,然后返回一个新的函子,里面包含的值是被f处理过的(f(this.val))。

一般约定,函子的标志就是容器具有map方法。该方法将容器里面的每一个值,映射到另一个容器。

学习函数式编程,实际上就是学习函子的各种运算。