用JavaScript理解Functor,Applicative和Monad

NO IMAGE

在之前的文章中,我們用圖片的形式來解釋了FunctorApplicativeMonad,但還是太抽象了,現在讓我們用JavaScript來繼續說明這些概念。

容器

任何值都可以被放入一個上下文中。這個值就好像被放入了盒子中,我們不能直接操作這個值。

用JavaScript理解Functor,Applicative和Monad

如圖,在上下文(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]

用JavaScript理解Functor,Applicative和Monad

這時,我們就需要一個方法讓別的函數能夠操作這個值:

// (a -> b) -> Just a -> Just b
Just.prototype.map = function(f){
return Just.of(f(this.__value))
}

上面的代碼中,map函數接受兩個參數,返回一個容器:

  • 第一個參數是函數(a-> b): 這個函數接受一個變量a,返回一個變量b,這個ab的類型可能相同,可能不同。

這裡變量指沒有放在上下文中的值。

  • 第二個參數是數據類型Just,這個Just中封裝著類型和a相同的值,和(a -> b)中的 a相對應。
  • 返回值是數據類型Just,這個Just中封裝著類型和b相同的值,和(a -> b)中的b相對應。

此時,我們就可以使用map函數來操作上下文裡的值了:

Just.of(2).map((a) => a + 3)
// Just { __value: 5 }

過程如下:

用JavaScript理解Functor,Applicative和Monad

我們使用map方法來操作數據,是為了在數據(比如2)不脫離數據類型(比如Just)的情況下,就可以操作數據,操作結束後,為了防止意外再把它放回它所屬的容器(Just)。這樣,我們能連續地調用map,運行任何我們想運行的函數。甚至還可以改變值的類型。

此時,Just就是一個Functor,它不僅是一種容器類型,也可以使用map 將一個函數運用到一個封裝的值上。

所以,functor是實現了map函數並遵守一些特定規則的容器類型。

map方法應該是泛指實現了能操作容器裡的值的方法, 下面MonadApplicative的定義同樣如此。

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,保證了一種線性的工作流,不必擔心錯誤的數據造成代碼拋出錯誤。

用JavaScript理解Functor,Applicative和Monad

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 }

這時,我們又將一個被封裝過的值運用到一個普通函數上,這又回到了我們最開始的時候。

用JavaScript理解Functor,Applicative和Monad

如果此時我們想操作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' }

而對於halfJust 3),Monad是這樣處理的:

用JavaScript理解Functor,Applicative和Monad

理論

下面是一個組合(compose)函數:

const compose = function(f,g) {
return function(x) {
return f(g(x));
};
};

對於Monad有:

1. 結合律

 // 結合律
compose(join, map(join)) == compose(join, join)

用圖表示則是:

用JavaScript理解Functor,Applicative和Monad

從左上角往下,先用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

用圖表示則是:

用JavaScript理解Functor,Applicative和Monad

如果從左上角開始往右,可以看到of的確把M a丟到另一個M 容器裡去了。然後再往下join,就得到了M a,跟一開始就調用id的結果一樣。從右上角往左,可以看到如果我們通過map進到了M 裡面,然後對普通值a調用of,最後得到的還是M (M a);再調用一次join將會把我們帶回原點,即M a

Applicative

Functor可以將封裝到上下文裡的值運用到普通函數上:

用JavaScript理解Functor,Applicative和Monad

那如果(+3)函數也被封裝在容器中:

用JavaScript理解Functor,Applicative和Monad

那麼此時,對容器Just裡面的值進行加3操作,就變成了:

用JavaScript理解Functor,Applicative和Monad

此時是兩個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))

用圖表示則是:

用JavaScript理解Functor,Applicative和Monad

上面實際表示的是map一個f等價於ap一個值為ffunctor

總結:

用JavaScript理解Functor,Applicative和Monad

  • Functor:你可以使用map將一個函數運用到一個封裝的值上
  • Applicative:你可以使用ap 將一個封裝過的函數運用到一個封裝的值上
  • Monad:你可以使用join將一個返回封裝值的函數運用到一個封裝的值上

參考文獻

JS函數式編程指南

Functors, Applicatives, And Monads In Pictures

相關文章

基於React實現高度簡潔的Form表單方案

移動端中跳轉支付寶、微信

【stepbystep】使用Vue封裝一個表單校驗

Vue源碼解讀(一):響應式數據