函数式编程中的functor和monad

以前接触函数式编程的时候听过functor(函子)和monad(单子),但也仅仅是了解,不常使用或者不知觉可能就在用了。后来学习Rust后发现它也将这套函数式的东西融入了语法当中,也让我加深了使用和理解。

要想了解什么是functor和monad,得先了解范畴论。

范畴论基本概念

我第一次听说范畴论(category theory)的时候被这高大上的名字吓到了,听起来像一是门复杂的学科。

函数式编程起源于范畴学,要想理解函数式编程,就要理解范畴学。

一个“范畴(category)”主要包含两个玩意

  • 东西(object)
  • 映射关系(morphism)

范畴学抽象到足以模拟任何事物,不过目前我们只关心的是类型和函数。我们可以把“范畴”理解成一个盒子,里面包含一个值和映射关系。

在Rust的标准库有这样一种结构,它用来表示值不存在的可能,同时也是可以看成是范畴的盒子。

1
2
3
4
pub enum Option<T> {
None,
Some(T),
}
1
2
3
let mut a = Some(2);
println!(a.map(|i| i + 3));
//=> Some(5)

在Javascript中,Promise也可以看成一个范畴。then返回了一个新的Promise,这个Promise所包裹的值是全新的值。

1
2
const a = Promise.resolve(2)
const b = a.then(|i| i + 3)

functor(函子)

当一个值被一个盒子包裹时,你不能简单使用函数应用到这个值。

这就需要map的用途(Option::map in Rust),map知道如何将普通函数应用到一个盒子中,比如上述Option的例子,通过接收一个外部指定的函数,返回一个新的函子,里面的值可能是被函数处理过的。当这个函子为None时直接返回None

函子就是让我们可以通过普通的函数将一个范畴转成另一个范畴。函数式编程里面的运算,都是通过函子完成,它的运算不直接针对值,而是针对这个值的盒子。我们甚至可以通过多种运算,衍生出多种函子,通过这些函子来解决实际问题。

用Javascript实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Container {
constructor(x) {
this._value = x
}
map(f) {
return Container.of(f(this._value))
}
static of(x) {
return new Container(x)
}
}

Container.of(2).map(two => two + 2)
//=> Container(4)

Maybe函子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Maybe {
constructor(x) {
this._value = x
}
isNothing() {
return !!this._value
}
map(f) {
return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this._value))
}
static of(x) {
return new Maybe(x)
}
}

Maybe.of(null).map(two => two + 2)
//=> Maybe(null)

Maybe看起来跟Container类似,不同的是,Maybe会先检查自己的值是否为空,然后再调用传进来的函数。这样就能避免处理空值的情况了。

Rust中的Option::map就体现了Maybe函子。

1
2
3
4
5
6
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U> {
match self {
Some(x) => Some(f(x)),
None => None,
}
}

Either函子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Either {
constructor(x, y) {
this._x = x
this._y = y
}
map(f) {
return this._x ? Either.of(this._x, this._y) : Either.of(this._x, f(this._y))
}
static of(x, y) {
return new Either(x, y)
}
}

Either.of(null, 2).map(two => two + 1)
//=> Either { _x: null, _y: 3 }

Either函子包含了左值(Left)和右值(Right),Left无视了map的请求,Right就是一个Container,这样的优势在于Left可以内部嵌入一个错误消息。所以比较常用的用途是用来代替try catch。

1
2
3
4
5
6
7
function parseJSON(json) {
try {
return Either.of(null, JSON.parse(json));
} catch (e) {
return Either.of(e, null);
}
}

Rust标准库里的Rusult就很好的利用了Either。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pub enum Result<T, E> {
Ok(T),
Err(E),
}

use std::num::ParseIntError;

fn double_number(number_str: &str) -> Result<i32, ParseIntError> {
number_str.parse::<i32>().map(|n| 2 * n)
}

fn main() {
match double_number("10") {
Ok(n) => assert_eq!(n, 10),
Err(err) => println!("Error: {:?}", err),
}
}

现在想想,Promise里你能看到Either的影子吗?

applicative函子

applicative函子里面包裹的值是一个函数,它定义了一个ap函数,能够把一个functor的函数值应用到另一个functor的值上。

1
2
3
4
5
6
7
8
class Ap {
ap(F) {
return Ap.of(this.val(F.val));
}
}

Ap.of(two => two + 2).ap(Container.of(2))
//=> Ap(4)

monad(单子)

函子funtor是将一个普通函数应用到包裹的值上,而单子monad则是将一个返回包裹值的函数应用到一个被包裹的值上。听起来有点拗口,这里举个例子。

Option::and_then,在某些语言里也叫flatmap

1
2
3
4
5
6
pub fn and_then<U, F: FnOnce(T) -> Option<U>>(self, f: F) -> Option<U> {
match self {
Some(x) => f(x),
None => None,
}
}

Option::map相比,map接收的函数返回的是具体的值,而and_then的则是一个Option,这样就出现了两层相同类型的嵌套,这种结合的能力,functor之间的联合,就是monad之所以成为monad的原因。

Rust的Iterator也是很好的实现了flat_map

1
2
3
4
5
6
7
let words = ["alpha", "beta", "gamma"];

// chars() 返回一个iterator,iterator也是一个函子
let merged: String = words.iter()
.flat_map(|s| s.chars())
.collect();
assert_eq!(merged, "alphabetagamma");

总结

虽然Javascript在模拟Haskell或者Rust这类语言的函数式语法特性比较笨拙,当然Javascript很多东西都简单为主,但函数式编程本身蕴含很多有趣的设计理念是值得探讨的。

参考

维基百科-范畴学
阮一峰-函数式编程入门教程
JS函数式编程指南
范畴学装逼指南