今天的夜點心關於函數式編程中的 Monad 函子javascript
函數式編程(下面簡稱 FP),每每被前端們拿來主義地用來解決一些「局部困難」:如使用 rxjs
來處理訂閱流;如使用高階組件來複用邏輯。它在充斥着反作用的應用中默默承擔着一個工具的角色,幫上一點小忙,卻不太受重視,還時常被曲解。html
「函數式夜點心」系列但願從動機出發,剝去一些干擾視聽的細節和定義,介紹一些 FP 中的概念。但願籍此能讓筆者和你們一塊兒對 FP 的優點和困境有更深刻的認識。前端
今天要介紹的概念 Monad (可譯做「單子函子」)是爲了解決「函數組合」和「異常處理」這兩個問題而引入的概念。java
現有以下兩個函數 f
和 g
,它們都輸入一個數字並輸出一個數字。咱們不關心他們的邏輯細節,而僅經過一種簡潔的方式來聲明他們的輸入輸出類型:git
f::Number -> Number
g::Number -> Number
複製代碼
如今咱們但願把他們組合起來,獲得一個新的函數 h
,讓它也成爲一個輸入數字輸出一個數字的函數:github
h::Number -> Number
複製代碼
這很簡單,構造一個用於組合函數的 compose
工具函數就能夠了,好比像下面這樣:編程
let compose = (func1, func2) => x => func1(func2(x))
let h = compose(g, f);
複製代碼
上面的函數 f
, g
, h
看起來一切正常,可是咱們不能保證輸入到它們的值必定合法,有可能輸入空值致使報錯。FP 認爲異常處理不該該打斷一段邏輯的執行,因此採用 try-catch 語句來抓錯是不可行的。爲了作到在處理異常的同時不打斷執行,咱們須要經過一種「容器」將函數的結果包裝起來,用來標明一個結果是否是存在異常。這裏咱們把這種容器命名爲 Maybe
,由於它具備某種未知性。這個容器能夠具備以下相似咱們熟悉的 AJAX 響應數據的結構:數組
interface Maybe<T> { // Maybe 的結構能夠像一個 AJAX 請求的響應數據同樣
error?: boolean; // error 代表執行過程是否是有異常
data?: T; // 成功時返回的執行結果
}
複製代碼
如今咱們就能夠改造 f
和 g
兩個函數,讓它們返回包含了數字結果的 Maybe
結構。下面聲明中的 Maybe Number
表明包裝了數字類型的 Maybe
容器:promise
mf::Number -> Maybe Number
mg::Number -> Maybe Number
複製代碼
於此同時咱們但願由他們組合獲得的 mh
函數也具備相同的輸入輸入出類型:安全
mh::Number -> Maybe Number
複製代碼
這時原來的組合函數 compose
就不能知足把 mf
和 mg
組合成 mh
的需求了,由於 mf
的返回結果是 Maybe
類型的,不能直接輸入給接收數字的 mg
處理。咱們須要一個新的組合函數,姑且把它稱做 mcompose
,先不用關心它的實現,只要知道它可以把 mf
和 mg
組合成 mh
就好了:
let mh = mcompose(mg, mf);
複製代碼
到這裏,對於一些函數式語言而言,其實咱們已經實現了所謂的 Monad:在對上面咱們定義的結構 Maybe
實現了 mcompose
操做以後,Maybe
就成爲一個 Monad 了,就是這麼簡單。
但對於 ES 而言,咱們仍是須要將上述的組合過程改寫爲鏈式調用的形式來方便你們理解。把 mf
和 mg
組合成 mh
的邏輯改寫成以下的鏈式結構:
let mh = x => Maybe.of(x).chain(mf).chain(mg)
複製代碼
這裏的 Maybe
在原先持有 data
和 error
字段的基礎上得到了一些額外的方法:
of
方法把輸入一個數字,輸出包裝持有該數字的一個 Maybe
結構chain
方法經過輸入的函數(該函數符合 Number -> Maybe Number
的結構),對自身持有的值進行處理,輸出一個持有新的結果的 Maybe
實例此外咱們的 Maybe
還須要實現一個 map
方法,來方便咱們將原來輸出數字的 f
和 g
轉爲爲輸出 Maybe
的 mf
和 mg
:
let mf = x => Maybe.of(x).map(f)
let mg = x => Maybe.of(x).map(g)
複製代碼
好了!像上面這樣實現了 of
, map
, chain
方法且可以持有值的對象,就被稱爲 Monad。它能幫助咱們解決「函數組合」和「異常處理」的問題,讓咱們能夠自由安全地組合邏輯,作到函數粒度的邏輯複用:
mh(null) // { error: true };
mh(1) // { data: {正確的返回值} };
mh(1).chain(mh) // 自我組合
mh(1).chain(
x => Maybe.of(x).map(x => x + 1)
); // 是否是有點流的感受了
複製代碼
在原生的 ECMAScript 語言中有沒有 Monad 呢?咱們熟知的 Array
就是一個,只是它的動機不在「異常處理」,並且它實現的鏈式方法不叫 chain
而叫 flatMap
,下面以 Array
爲例替換上文中的 Maybe
:
let f = x => x + 1
let g = x => x ** 2
let mf = x => Array.of(x).map(f)
let mg = x => Array.of(x).map(g)
let mh = x => Array.of(x).flatMap(mf).flatMap(mg);
複製代碼
Array
做爲 Monad 爲咱們提供了「批量處理數據」和「組合邏輯」的能力。
那另外一個重要的 ES 對象 Promise
是否關於 then
方法成爲 Monad 的呢?答案是否認的,根本緣由在於,Promise
的 then
便可以像 map
那樣直接處理相似上面 f
這樣的函數,又能像 chain
那樣處理 mf
那樣的函數,它混淆了兩個概念,這樣的混淆會形成一些本來在其餘 Monad 上成立的「重構等式」在 Promise
上不成立,故嚴格來講,不能把它算做 Monad (詳見 stackoverflow - Why are Promises Monads? 下的第一個回答)。
最後,Monad 是流的雛形。各類流式框架的核心結構都是 Monad ,例如 rx 中的 Observable
,xstream 中的 XStream
,而 most 框架的名字就是由 Monadic Stream 的首字母 mo 和 st 構成的。
爲了方便解釋,文中簡化和減小一些概念,在這裏作一下補充:
Maybe
不會像文中那樣定義成響應體的結構,而是被分解爲兩個構造器 Just
和 Nothing
,前者用來包含結果,後者用來表示異常。如在 Haskell 中能夠定義爲 data Maybe a = Just a | Nothing
。有的語言或框架把這種異常處理的結構命名爲 Either
,分爲 Right
(正常)和 Left
(異常)兩個構造器:data Either a = Right a | Left
。mcompose
方法等同於操做符 >=>
,相似的 Monad 操做符還有 >>=
, >>
,都是與具體的 Monad 分離的方法。這代表咱們並不須要把數據和方法綁定在一塊兒才能讓 Monad 成立,只是在 ES 等多範式語言中,經過類來實現 Monad 是最天然的方式。chain
的定義是:M a -> (a -> M b) -> b
,便可以將一個包裝了 a
類型的結構經過具備 a -> M b
的結構函數,映射獲得一個包裝了 b
類型的結構。點下方原文連接,能夠在 github 中看到對 Maybe
ES 的實現