前端領域中許多老生常談的話題背後,其實都蘊含着經典的計算機科學基礎知識。在今天,只要你使用 JS 發起過網絡請求,那其實你基本就使用過了函數式編程中的 Monad。這是怎麼一回事呢?讓咱們從回調地獄提及吧……前端
熟悉 JS 的同窗對於回調函數必定不會陌生,這是這門語言中處理異步事件最經常使用的手法。然而正如咱們所熟知的那樣,順序處理多個異步任務的工做流很容易形成回調的嵌套,使得代碼難以維護:ios
$.get(a, (b) => {
$.get(b, (c) => {
$.get(c, (d) => {
console.log(d)
})
})
})
複製代碼
長久以來這個問題一直困擾着廣大 JSer,社區的解決方案也是百花齊放。其中一種已經成爲標準的方案叫作 Promise,你能夠將異步回調包在 Promise 裏,由 Promise.then
方法鏈式組合異步工做:git
const getB = a =>
new Promise((resolve, reject) => $.get(a, resolve))
const getC = b =>
new Promise((resolve, reject) => $.get(b, resolve))
const getD = c =>
new Promise((resolve, reject) => $.get(c, resolve))
getB(a)
.then(getC)
.then(getD)
.then(console.log)
複製代碼
雖然 ES7 裏已經有了更簡練的 async/await 語法,但 Promise 已經有了很是普遍的應用。好比,網絡請求的新標準 fetch 會將返回內容封裝爲 Promise,目前最流行的 Ajax 庫 axios 也是這麼作的。至於一度佔領 70% 網頁的元老基礎庫 jQuery,早在 1.5 版本中就支持了 Promise。這就意味着,只要你在前端發起過網絡請求,你基本上就和 Promise 打過交道。而 Promise 自己,就是一種 Monad。github
不過,各種對 Promise 的介紹多半集中在它的各類狀態遷移和 API 使用上,這和 Monad 聽起來彷佛徹底八竿子打不着,這兩個概念之間有什麼聯繫呢?要講清楚這個問題,咱們至少得搞懂 Monad 是什麼。算法
不少原本有興趣學習 Haskell 等函數式語言的同窗,均可能被一句名言震懾到打退堂鼓——【Monad 不就是自函子上的幺半羣嗎,有什麼難以理解的】。其實這句話和白學家說的【冬馬小三,雪菜碧池】沒有什麼差異,不過是一句正確的廢話而已,聽完懂的人仍是懂,不懂的人仍是不懂。因此若是再有人和你這麼介紹 Monad,請放心地打死他吧——喂等等,誰說冬三雪碧是正確的了!編程
迴歸正題,Monad 究竟是什麼呢?咱們大可沒必要拿出 PLT 或 Haskell 那一套,而是在 JS 的語境裏好好考慮一下這個問題:既然 Promise 在 JS 裏是一個對象,相似地,你也能夠把 Monad 當作一個特殊的對象。axios
既然是對象,那麼它的黑魔法也不外乎存在於屬性和方法兩個地方里了。下面咱們要回答一個相當重要的問題:Monad 有什麼特殊的屬性和方法,來幫助咱們逃離回調地獄呢?promise
咱們能夠用很是簡單的僞代碼來澄清這個問題。假如咱們有 A B C D 四件事要作,那麼基於回調嵌套,你能夠寫出最簡單的函數表達式形如:網絡
A(B(C(D)))
複製代碼
看到嵌套回調的噩夢了吧?不過,咱們能夠抽絲剝繭地簡化這個場景。首先,咱們把問題簡化到最普通的回調嵌套:異步
A(B)
複製代碼
基於添加中間層和控制反轉的理念,咱們只需十幾行代碼,就可以實現一個簡單的中間對象 P,把 A 和 B 分開傳給這個對象,從而把回調拆分開:
P(A).then(B)
複製代碼
如今,A 被咱們包裝了一層,P 這個容器就是 Promise 的雛形了!在筆者的博文 從源碼看 Promise 概念與實現 中,已經解釋了這樣將回調嵌套解除的基本機制了,相應的代碼實如今此再也不贅述。
可是,這個解決方案只適用於 A B 兩個函數之間發生嵌套的場景。只要你嘗試去實現過這個版本的 P,你必定會發現,咱們如今沒有這種能力:
P(A).then(B).then(C).then(D)
複製代碼
也沒有這種能力:
P(P(P(A))).then(B)
複製代碼
這就是 Monad 大展身手的時候了!咱們首先給出答案: Monad 對象是這個簡陋版 P
的強化,它的 then
能支持這種嵌套和鏈式調用的場景。 固然,正統的 Monad 裏這個 API 不是這個名字,但做爲參照,咱們能夠先看看 Promise/A+ 規範中的一個關鍵細節:
在每次 Resolve 一個 Promise 時,咱們須要判斷兩種狀況:
thenable
),那麼遞歸 Resolve 這個 Promise。fulfill
或 reject
當前 Promise。直觀地說,這個細節可以保證下面兩種調用方式徹底等效:
// 1
Promise.resolve(1).then(console.log)
// 1
Promise.resolve(
Promise.resolve(
Promise.resolve(
Promise.resolve(1)
)
)
).then(console.log)
複製代碼
這裏的嵌套是否似曾相識?這實際上就是披着 Promise 外衣的 Monad 核心能力:對於一個 P 這樣裝着某種內容的容器,咱們可以遞歸地把容器一層層拆開,直接取出最裏面裝着的值。只要實現了這個能力,經過一些技巧,咱們就可以實現下面這個優雅的鏈式調用 API:
Promise(A).then(B).then(C).then(D)
複製代碼
這更帶來了額外的好處:無論這裏面的 B C D 函數返回的是同步執行的值仍是異步解析的 Promise,咱們都能徹底一致地處理。好比這個同步的加法:
const add = x => x + 1
Promise
.resolve(0)
.then(add)
.then(add)
.then(console.log)
// 2
複製代碼
和這個略顯擰巴的異步加法:
const add = x =>
new Promise((resolve, reject) => setTimeout(() => resolve(x + 1), 1000))
Promise
.resolve(0)
.then(add)
.then(add)
.then(console.log)
// 2
複製代碼
不分同步與異步,它們的調用方式與最終結果徹底一致!
做爲一個總結,讓咱們看看從回調地獄到 Promise 的過程當中,背後運用了哪些函數式編程中的概念呢?
P(A).then(B)
實現裏,它的 P(A)
至關於 Monad 中的 unit
接口,可以把任意值包裝到 Monad 容器裏。then
背後實際上是 FP 中的 join
概念,在容器裏還裝着容器的時候,遞歸地把內層容器拆開,返回最底層裝着的值。bind
概念。你能夠扁平地串聯一堆 .then()
,往裏傳入各類函數,Promise 可以幫你抹平同步和異步的差別,把這些函數逐個應用到容器裏的值上。迴歸這節中最原始的問題,Monad 是什麼呢?只要一個對象具有了下面兩個方法,咱們就能夠認爲它是 Monad 了:
unit
。bind
(這和 JS 裏的 bind
徹底是兩個概念,請不要混淆了)。正如咱們已經看到的,Promise.resolve()
可以把任意值包裝到 Promise 裏,而 Promise/A+ 規範裏的 Resolve 算法則實際上實現了 bind
。所以,咱們能夠認爲:Promise 就是一個 Monad。其實這並非一個新奇的結論,在 Github 上早有人從代碼角度給出了證實,有興趣的同窗能夠去感覺一下 :-)
做爲總結,最後考慮這個問題:咱們是怎麼把 Promise 和 Monad 聯繫起來呢?Promise 消除回調地獄的關鍵在於:
A(B)
爲 P(A).then(B)
的形式。這其實就是 Monad 用來構建容器的 unit
。P(A).then(B).then(C)...
,這實際上是 Monad 裏的 bind
。到這裏,咱們就可以從 Promise 的功能來理解 Monad 的做用,並用 Monad 的概念來解釋 Promise 的設計啦 😉
到了這裏,只要你理解了 Promise,那麼你應該就已經能夠理解 Monad 了。不過,Monad 傳說中【自函子上的幺半羣】又是怎麼一回事呢?其實只要你讀到了這裏,你就已經見識過自函子和幺半羣了(這裏的理解未必準確,權當拋磚引玉之用,但願 dalao 指正)。
函子即所謂的 Functor,是一個能把值裝在裏面,經過傳入函數來變換容器內容的容器:簡化的理解裏,前文中的 Promise.resolve
就至關於這樣的映射,能把任意值裝進 Promise 容器裏。而自函子則是【能把範疇映射到自己】的 Functor,能夠對應於 Promise(A).then()
裏仍然返回 Promise 自己。
幺半羣即所謂的 Monadic,知足兩個條件:單位元與結合律。
單位元是這樣的兩個條件:
首先,做用到單位元 unit(a)
上的 f
,結果和 f(a)
一致:
const value = 6
const f = x => Promise.resolve(x + 6)
// 下面兩個值相等
const left = Promise.resolve(value).then(f)
const right = f(value)
複製代碼
其次,做用到非單位元 m
上的 unit
,結果仍是 m
自己:
const value = 6
// 下面兩個值相等
const left = Promise.resolve(value)
const right = Promise.resolve(value).then(x=> Promise.resolve(x))
複製代碼
至於結合律則是這樣的條件:(a • b) • c
等於 a • (b • c)
:
const f = a => Promise.resolve(a * a)
const g = a => Promise.resolve(a - 6)
const m = Promise.resolve(7)
// 下面兩個值相等
const left = m.then(f).then(g)
const right = m.then(x => f(x).then(g))
複製代碼
上面短短的幾行代碼,其實就是對【Promise 是 Monad】的一個證實了。到這裏,咱們能夠發現,平常對接接口編寫 Promise 的時候,咱們寫的東西均可以先提高到函數式編程的 Monad 層面,而後用抽象代數和範疇論來解釋,逼格是否是瞬間提升了呢 XD
上面全部的論證都沒有牽扯到 >>==
這樣的 Haskell 內容,咱們能夠徹底用 JS 這樣低門檻的語言來介紹 Monad 是什麼,又有什麼用。某種程度上筆者認同王垠的觀點:函數式編程的門檻被人爲地拔高或神話了,明明是實際開發中很是實用且易於理解的東西,卻要使用更難以懂的一套概念去形式化地定義和解釋,這恐怕並不利於優秀工具和理念的普及。
固然了,爲了體現逼格,若是下次再有同窗問你 Promise 是什麼,請這麼回覆:
Promise 不就是自函子上的幺半羣嗎,有什麼難以理解的 🙂
最後插播廣告:筆者寫這篇文章的動機,是源自實現一個徹底 Promise 化的異步數據轉換輪子 Bumpover 時對 Promise 的一些新理解。有興趣的同窗歡迎關注哦 XD