以前接触函数式编程的时候听过functor(函子)和monad(单子),但也仅仅是了解,不常使用或者不知觉可能就在用了。后来学习Rust后发现它也将这套函数式的东西融入了语法当中,也让我加深了使用和理解。
要想了解什么是functor和monad,得先了解范畴论。
范畴论基本概念
我第一次听说范畴论(category theory)的时候被这高大上的名字吓到了,听起来像一是门复杂的学科。
函数式编程起源于范畴学,要想理解函数式编程,就要理解范畴学。
一个“范畴(category)”主要包含两个玩意
- 东西(object)
- 映射关系(morphism)
范畴学抽象到足以模拟任何事物,不过目前我们只关心的是类型和函数。我们可以把“范畴”理解成一个盒子,里面包含一个值和映射关系。
在Rust的标准库有这样一种结构,它用来表示值不存在的可能,同时也是可以看成是范畴的盒子。
1 | pub enum Option<T> { |
1 | let mut a = Some(2); |
在Javascript中,Promise也可以看成一个范畴。then
返回了一个新的Promise,这个Promise所包裹的值是全新的值。
1 | const a = Promise.resolve(2) |
functor(函子)
当一个值被一个盒子包裹时,你不能简单使用函数应用到这个值。
这就需要map
的用途(Option::map
in Rust),map
知道如何将普通函数应用到一个盒子中,比如上述Option
的例子,通过接收一个外部指定的函数,返回一个新的函子,里面的值可能是被函数处理过的。当这个函子为None
时直接返回None
。
函子就是让我们可以通过普通的函数将一个范畴转成另一个范畴。函数式编程里面的运算,都是通过函子完成,它的运算不直接针对值,而是针对这个值的盒子。我们甚至可以通过多种运算,衍生出多种函子,通过这些函子来解决实际问题。
用Javascript实现:
1 | class Container { |
Maybe函子
1 | class Maybe { |
Maybe看起来跟Container类似,不同的是,Maybe会先检查自己的值是否为空,然后再调用传进来的函数。这样就能避免处理空值的情况了。
Rust中的Option::map
就体现了Maybe函子。
1 | pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U> { |
Either函子
1 | class Either { |
Either函子包含了左值(Left)和右值(Right),Left无视了map
的请求,Right就是一个Container,这样的优势在于Left可以内部嵌入一个错误消息。所以比较常用的用途是用来代替try catch。
1 | function parseJSON(json) { |
Rust标准库里的Rusult
就很好的利用了Either。
1 | pub enum Result<T, E> { |
现在想想,Promise里你能看到Either的影子吗?
applicative函子
applicative函子里面包裹的值是一个函数,它定义了一个ap
函数,能够把一个functor的函数值应用到另一个functor的值上。
1 | class Ap { |
monad(单子)
函子funtor是将一个普通函数应用到包裹的值上,而单子monad则是将一个返回包裹值的函数应用到一个被包裹的值上。听起来有点拗口,这里举个例子。
Option::and_then
,在某些语言里也叫flatmap
。
1 | pub fn and_then<U, F: FnOnce(T) -> Option<U>>(self, f: F) -> Option<U> { |
与Option::map
相比,map接收的函数返回的是具体的值,而and_then的则是一个Option,这样就出现了两层相同类型的嵌套,这种结合的能力,functor之间的联合,就是monad之所以成为monad的原因。
Rust的Iterator
也是很好的实现了flat_map
。
1 | let words = ["alpha", "beta", "gamma"]; |
总结
虽然Javascript在模拟Haskell或者Rust这类语言的函数式语法特性比较笨拙,当然Javascript很多东西都简单为主,但函数式编程本身蕴含很多有趣的设计理念是值得探讨的。