在之前的文章中,我们用图片的形式来解释了Functor
、Applicative
和Monad
,但还是太抽象了,现在让我们用JavaScript
来继续说明这些概念。
# 容器
任何值都可以被放入一个上下文中。这个值就好像被放入了盒子中,我们不能直接操作这个值。
如图,在上下文(content
)中,封装着一个值2
。实现这个盒子的代码:
const Just = function(x) {
this.__value = x;
}
Just.of = function(x) {
return new Just(x);
};
在上面的代码中,数据类型Just
形成了一个上下文,在这个上下文中,有属性__value
用来保存被放入的值。在数据类型Just
上有of
方法,它作为Just
的构造器。
of
方法不仅用来避免使用new
关键字的,而且还用来把值放到默认最小化上下文(default minimal context
)中的。
让我们看看这个盒子:
Just.of(3)
// Just { __value: 3 }
Just.of('hotdogs')
// Just { __value: 'hotdogs' }
Just.of(Just.of({ name: 'yoda' }))
// Just { __value: Just { __value: { name: 'yoda' } } }
上面的结果是用
node
打印出来的,下面的同样如此。
# Functor
当一个值被封装在一个盒子中,我们不能直接操作这个值:
const Just = function (x) {
this.__value = x;
}
Just.of = function (x) {
return new Just(x);
};
function add(x) {
return 3 + x;
}
add(Just.of(2))
// 3[object Object]
这时,我们就需要一个方法让别的函数能够操作这个值:
// (a -> b) -> Just a -> Just b
Just.prototype.map = function(f){
return Just.of(f(this.__value))
}
上面的代码中,map
函数接受两个参数,返回一个容器:
- 第一个参数是函数(
a-> b
): 这个函数接受一个变量a
,返回一个变量b
,这个a
和b
的类型可能相同,可能不同。
这里变量指没有放在上下文中的值。
- 第二个参数是数据类型
Just
,这个Just
中封装着类型和a
相同的值,和(a -> b
)中的a
相对应。 - 返回值是数据类型
Just
,这个Just
中封装着类型和b
相同的值,和(a -> b
)中的b
相对应。
此时,我们就可以使用map
函数来操作上下文里的值了:
Just.of(2).map((a) => a + 3)
// Just { __value: 5 }
过程如下:
我们使用map
方法来操作数据,是为了在数据(比如2
)不脱离数据类型(比如Just
)的情况下,就可以操作数据,操作结束后,为了防止意外再把它放回它所属的容器(Just
)。这样,我们能连续地调用map
,运行任何我们想运行的函数。甚至还可以改变值的类型。
此时,Just
就是一个Functor
,它不仅是一种容器类型,也可以使用map
将一个函数运用到一个封装的值上。
所以,functor
是实现了map
函数并遵守一些特定规则的容器类型。
map
方法应该是泛指实现了能操作容器里的值的方法, 下面Monad
和Applicative
的定义同样如此。
# Maybe
上下文中可以放入任意的值,当然也就可以放入falsy
值,我们叫这个容器为Maybe
。那么运用其他函数来操作里面的值时,有可能会抛出错误,所以我们可以在Maybe
里面进行容错处理。
const Maybe = function(x) {
this.__value = x;
}
Maybe.of = function(x) {
return new Maybe(x);
}
Maybe.prototype.isNothing = function() {
return (this.__value === null || this.__value === undefined);
}
Maybe.prototype.map = function(f) {
return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
}
Maybe
看起来跟其他容器非常类似,但是有一点不同:Maybe
会先检查自己的值是否为空,然后才调用传进来的函数。
Maybe.of(null).map((a) => a + 3)
// Just { __value: null }
当传给map
的值是null
时,代码并没有爆出错误。这样我们就能连续使用map
,保证了一种线性的工作流,不必担心错误的数据造成代码抛出错误。
# Monad
这是我们上面定义的容器:
const Just = function (x) {
this.__value = x;
}
Just.of = function (x) {
return new Just(x)
}
当我们想操作容器里的值时,我们可以这样做:
Just.prototype.map = function (f) {
return Just.of(f(this.__value))
}
const half = function (x) {
return x / 2
}
如果此时容器是这样的:
Just.of(3).map(half)
此时容器里面封装着值3
,我们使用map
操作它的值是没有问题的。这就是上面讲的Functor
但如果此时,容器是这样的:
const nestedContainer = Just.of(Just.of(3))
// Just { __value: Just { __value: 3 } }
此时,如果我们使用map
来操作nestedContainer
容器里的值是不可能的:
nestedContainer.map(half)
此时回调函数half
参数为:
Just { __value: 3 }
这时,我们又将一个被封装过的值运用到一个普通函数上,这又回到了我们最开始的时候。
如果此时我们想操作nestedContainer
容器里的值,那我们就需要Monad
:
monad
是可以变扁(flatten
)的pointed functor
。
pointed functor
是实现了of
方法的functor
。
我们来为Maybe
定义一个join
方法,让它成为称为一个Monad
:
// m a -> (a -> m b) -> m b
Maybe.prototype.join = function() {
return this.isNothing() ? Maybe.of(null) : this.__value;
}
const mmo = Maybe.of(Maybe.of("nunchucks"));
// Maybe { __value: Maybe { __value: 'nunchucks' } }
mmo.join();
// Maybe { __value: 'nunchucks' }
而对于half
(Just 3
),Monad
是这样处理的:
# 理论
下面是一个组合(compose
)函数:
const compose = function(f,g) {
return function(x) {
return f(g(x));
};
};
对于Monad
有:
# 1. 结合律
// 结合律
compose(join, map(join)) == compose(join, join)
用图表示则是:
从左上角往下,先用join
合并M(M(M a))
最外层的两个 M
,然后往右,再调用一次join
,就得到了我们想要的M a
。或者,从左上角往右,先打开最外层的M
,用map(join)
合并内层的两个 M
,然后再向下调用一次join
,也能得到M a
。不管是先合并内层还是先合并外层的M
,最后都会得到相同的M a
,所以这就是结合律。
# 2. 同一律
// 同一律 (M a)
compose(join, of) == compose(join, map(of)) == id
用图表示则是:
如果从左上角开始往右,可以看到of
的确把M a
丢到另一个M
容器里去了。然后再往下join
,就得到了M a
,跟一开始就调用id
的结果一样。从右上角往左,可以看到如果我们通过map
进到了M
里面,然后对普通值a
调用of
,最后得到的还是M (M a)
;再调用一次join
将会把我们带回原点,即M a
。
# Applicative
Functor
可以将封装到上下文里的值运用到普通函数上:
那如果(+3)
函数也被封装在容器中:
那么此时,对容器Just
里面的值进行加3
操作,就变成了:
此时是两个Functor
之间的交互,就需要用到Applicative
了。
我们先定义一个ap
方法,让它可以让两个functor
进行交互:
function add(x) {
return function (y) {
return x + y;
};
}
Just.prototype.ap = function (otherContainer) {
return otherContainer.map(this.__value)
}
Just.of(add(2)).ap(Just.of(3));
// Just { __value: 5 }
其中map
函数的参数this.__value
是一个函数。
所以Applicative
就可以定义为:
applicative functor
是实现了ap
方法的pointed functor
下面是一个特性:
M.of(a).map(f) = F.of(f).ap(M.of(a))
用图表示则是:
上面实际表示的是map
一个f
等价于ap
一个值为f
的functor
# 总结:
Functor
:你可以使用map
将一个函数运用到一个封装的值上Applicative
:你可以使用ap
将一个封装过的函数运用到一个封装的值上Monad
:你可以使用join
将一个返回封装值的函数运用到一个封装的值上
# 参考文献
Functors, Applicatives, And Monads In Pictures
(opens new window)