函數式夜點心:Monad

今天的夜點心關於函數式編程中的 Monad 函子javascript

函數式編程(下面簡稱 FP),每每被前端們拿來主義地用來解決一些「局部困難」:如使用 rxjs 來處理訂閱流;如使用高階組件來複用邏輯。它在充斥着反作用的應用中默默承擔着一個工具的角色,幫上一點小忙,卻不太受重視,還時常被曲解。html

「函數式夜點心」系列但願從動機出發,剝去一些干擾視聽的細節和定義,介紹一些 FP 中的概念。但願籍此能讓筆者和你們一塊兒對 FP 的優點和困境有更深刻的認識。前端

今天要介紹的概念 Monad (可譯做「單子函子」)是爲了解決「函數組合」和「異常處理」這兩個問題而引入的概念。java

問題描述

現有以下兩個函數 fg,它們都輸入一個數字並輸出一個數字。咱們不關心他們的邏輯細節,而僅經過一種簡潔的方式來聲明他們的輸入輸出類型: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; // 成功時返回的執行結果
}
複製代碼

如今咱們就能夠改造 fg 兩個函數,讓它們返回包含了數字結果的 Maybe 結構。下面聲明中的 Maybe Number 表明包裝了數字類型的 Maybe 容器promise

mf::Number -> Maybe Number

mg::Number -> Maybe Number
複製代碼

於此同時咱們但願由他們組合獲得的 mh 函數也具備相同的輸入輸入出類型:安全

mh::Number -> Maybe Number
複製代碼

這時原來的組合函數 compose 就不能知足把 mfmg 組合成 mh 的需求了,由於 mf 的返回結果是 Maybe 類型的,不能直接輸入給接收數字的 mg 處理。咱們須要一個新的組合函數,姑且把它稱做 mcompose,先不用關心它的實現,只要知道它可以把 mfmg 組合成 mh 就好了:

let mh = mcompose(mg, mf);
複製代碼

到這裏,對於一些函數式語言而言,其實咱們已經實現了所謂的 Monad:在對上面咱們定義的結構 Maybe 實現了 mcompose 操做以後,Maybe 就成爲一個 Monad 了,就是這麼簡單。

但對於 ES 而言,咱們仍是須要將上述的組合過程改寫爲鏈式調用的形式來方便你們理解。把 mfmg 組合成 mh 的邏輯改寫成以下的鏈式結構:

let mh = x => Maybe.of(x).chain(mf).chain(mg)
複製代碼

這裏的 Maybe 在原先持有 dataerror 字段的基礎上得到了一些額外的方法:

  • of 方法把輸入一個數字,輸出包裝持有該數字的一個 Maybe 結構
  • chain 方法經過輸入的函數(該函數符合 Number -> Maybe Number 的結構),對自身持有的值進行處理,輸出一個持有新的結果的 Maybe 實例

此外咱們的 Maybe 還須要實現一個 map 方法,來方便咱們將原來輸出數字的 fg 轉爲爲輸出 Maybemfmg

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)
); // 是否是有點流的感受了
複製代碼

ES 原生的 Monad

在原生的 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 的呢?答案是否認的,根本緣由在於,Promisethen 便可以像 map 那樣直接處理相似上面 f 這樣的函數,又能像 chain 那樣處理 mf 那樣的函數,它混淆了兩個概念,這樣的混淆會形成一些本來在其餘 Monad 上成立的「重構等式」在 Promise 上不成立,故嚴格來講,不能把它算做 Monad (詳見 stackoverflow - Why are Promises Monads? 下的第一個回答)

最後,Monad 是流的雛形。各類流式框架的核心結構都是 Monad ,例如 rx 中的 Observable,xstream 中的 XStream,而 most 框架的名字就是由 Monadic Stream 的首字母 mo 和 st 構成的。

補充

爲了方便解釋,文中簡化和減小一些概念,在這裏作一下補充:

  • 文中幾處用來描述函數類型的語法是一種叫作 Hindley–Milner 的類型系統
  • 真正的 Maybe 不會像文中那樣定義成響應體的結構,而是被分解爲兩個構造器 JustNothing,前者用來包含結果,後者用來表示異常。如在 Haskell 中能夠定義爲 data Maybe a = Just a | Nothing。有的語言或框架把這種異常處理的結構命名爲 Either,分爲 Right (正常)和 Left (異常)兩個構造器:data Either a = Right a | Left
  • 在 Haskell 中,上面的 mcompose 方法等同於操做符 >=> ,相似的 Monad 操做符還有 >>=, >>,都是與具體的 Monad 分離的方法。這代表咱們並不須要把數據和方法綁定在一塊兒才能讓 Monad 成立,只是在 ES 等多範式語言中,經過類來實現 Monad 是最天然的方式。
  • Monad 實際上是以一系列概念做爲基礎的,這些概念相互繼承,每一層會增長一些特性,文中把特性都集中直接到了 Monad 身上: Context(持有數據)=> Pointed Container(持有 of 方法)=> Functor(持有 map 方法)=> Monad(持有 chain 方法)。而對 chain 的定義是:M a -> (a -> M b) -> b,便可以將一個包裝了 a 類型的結構經過具備 a -> M b 的結構函數,映射獲得一個包裝了 b 類型的結構。

點下方原文連接,能夠在 github 中看到對 Maybe ES 的實現

github 原文連接

擴展閱讀

相關文章
相關標籤/搜索