在之前的文章中,我們用圖片的形式來解釋了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