上一篇文章中,咱們討論了經常使用的函數式編程案例,一些同窗反饋沒有講到底層概念,想了解一下什麼是 Monad?基於這個問題,咱們來探究一下。javascript
在函數式編程中,Monad 是一種結構化程序的抽象,咱們經過三個部分來理解一下。前端
根據維基百科的定義,Monad 由如下三個部分組成:java
M<T>
。一個類型轉換函數(return or unit),可以把一個原始值裝進 M 中。ajax
T -> M T
一個組合函數 bind,可以把 M 實例中的值取出來,放入一個函數中去執行,最終獲得一個新的 M 實例。編程
M<T>
執行 T-> M<U>
生成 M<U>
除此以外,它還遵照一些規則:promise
單位元:是 集合裏的一種特別的元素,與該集合裏的 二元運算有關。當單位元和其餘元素結合時,並不會改變那些元素。乘法的單位元就是 1,任何數 x 1 = 任何數自己、1 x 任何數 = 任何數自己。安全
加法的單位元就是 0,任何數 + 0 = 任何數自己、0 + 任何數 = 任何數自己。網絡
這些定義很抽象,咱們用一段 js 代碼來模擬一下。異步
class Monad { value = ""; // 構造函數 constructor(value) { this.value = value; } // unit,把值裝入 Monad 構造函數中 unit(value) { this.value = value; } // bind,把值轉換成一個新的 Monad bind(fn) { return fn(this.value); } } // 知足 x-> M(x) 格式的函數 function add1(x) { return new Monad(x + 1); } // 知足 x-> M(x) 格式的函數 function square(x) { return new Monad(x * x); } // 接下來,咱們就能進行鏈式調用了 const a = new Monad(2) .bind(square) .bind(add1); //... console.log(a.value === 5); // true
上述代碼就是一個最基本的 Monad,它將程序的多個步驟抽離成線性的流,經過 bind 方法對數據流進行加工處理,最終獲得咱們想要的結果。函數式編程
Ok,咱們已經明白了 Monad 的內部結構,接下來,咱們再看一下 Monad 的使用場景。
經過 Monad 的規則,衍生出了許多使用場景。
組裝多個函數,實現鏈式操做。
fn1(fn2(fn3()))
。處理反作用。
還記得 Jquery 時代的 ajax 操做嗎?
$.ajax({ type: "get", url: "request1", success: function (response1) { $.ajax({ type: "get", url: "request2", success: function (response2) { $.ajax({ type: "get", url: "request3", success: function (response3) { console.log(response3); // 獲得最終結果 }, }); }, }); }, });
上述代碼中,咱們經過回調函數,串行執行了 3 個 ajax 操做,但一樣也生成了 3 層代碼嵌套,這樣的代碼不只難以閱讀,也不利於往後維護。
Promise 的出現,解決了上述問題。
fetch("request1") .then((response1) => { return fetch("request2"); }) .then((response2) => { return fetch("request3"); }) .then((response3) => { console.log(response3); // 獲得最終結果 });
咱們經過 Promise,將多個步驟封裝到多個 then 方法中去執行,不只消除了多層代碼嵌套問題,並且也讓代碼劃分更加天然,大大提升了代碼的可維護性。
想想,爲何 Promise 能夠不斷執行 then 方法?
其實,Promise 和 Monad 很相似,它知足了多條 Monad 規則。
x => Promise.resolve(x)
Promise.prototype.then
咱們用代碼來驗證一下。
// 首先定義 2 個異步處理函數。 // 延遲 1s 而後 加一 function delayAdd1(x) { return new Promise((resolve) => { setTimeout(() => { resolve(x + 1); }); }, 1000); } // 延遲 1s 而後 求平方 function delaySquare(x) { return new Promise((resolve) => { setTimeout(() => { resolve(x * x); }); }, 1000); } /****************************************************************************************/ // 單位元 e 規則,知足:e*a = a*e = a const promiseA = Promise.resolve(2).then(delayAdd1); const promiseB = delayAdd1(2); // promiseA === promiseB,故 promise 知足左單位元。 const promiseC = Promise.resolve(2); const promiseD = a.then(Promise.resolve); // promiseC === promiseD,故 promise 知足右單位元。 // promise 既知足左單位元,又知足右單位元,故 Promise 知足單位元。 // ps:但一些特殊的狀況不知足該定義,下文中會講到 /****************************************************************************************/ // 結合律規則:(a * b)* c = a *(b * c) const promiseE = Promise.resolve(2); const promiseF = promiseE.then(delayAdd1).then(delaySquare); const promiseG = promiseE.then(function (x) { return delayAdd1(x).then(g); }); // promiseF === promiseG,故 Promise 是知足結合律。 // ps:但一些特殊的狀況不知足該定義,下文中會講到
看完上面的代碼,不由感受很驚訝,Promise 和 Monad 也太像了吧,不只能夠實現鏈式操做,也知足單位元和結合律,難道 Promise 就是一個 Monad?
其實否則,Promise 並不徹底知足 Monad:
若是是這兩種狀況,那就沒法知足 Monad 規則。
// Promise.resolve 傳入一個 Promise 對象 const functionA = function (p) { // 這時 p === 1 return p.then((n) => n * 2); }; const promiseA = Promise.resolve(1); Promise.resolve(promiseA).then(functionA); // RejectedPromise TypeError: p.then is not a function // 因爲 Promise.resolve 對傳入的 Promise 進行了處理,致使直接運行報錯。違背了單位元和結合律。 // Promise.resolve 傳入一個 thenable 對象 const functionB = function (p) { // 這時 p === 1 alert(p); return p.then((n) => n * 2); }; const obj = { then(r) { r(1); }, }; const promiseB = Promise.resolve(obj); Promise.resolve(promiseB).then(functionB); // RejectedPromise TypeError: p.then is not a function // 因爲 Promise.resolve 對傳入的 thenable 進行了處理,致使直接運行報錯。違背了單位元和結合律。
看到這裏,相信你們對 Promise 也有了一層新的瞭解,正是藉助了 Monad 同樣的鏈式操做,才使 Promise 普遍應用在了前端異步代碼中,你是否也和我同樣,對 Monad 充滿了好感?
接下來,咱們再看一個常見的問題:爲何 Monad 適合處理反作用?
ps:這裏說的反作用,指的是違反 純函數原則的操做,咱們應該儘量避免這些操做,或者把這些操做放在最後去執行。
例如:
var fs = require("fs"); // 純函數,傳入 filename,返回 Monad 對象 var readFile = function (filename) { // 反作用函數:讀取文件 const readFileFn = () => { return fs.readFileSync(filename, "utf-8"); }; return new Monad(readFileFn); }; // 純函數,傳入 x,返回 Monad 對象 var print = function (x) { // 反作用函數:打印日誌 const logFn = () => { console.log(x); return x; }; return new Monad(logFn); }; // 純函數,傳入 x,返回 Monad 對象 var tail = function (x) { // 反作用函數:返回最後一行的數據 const tailFn = () => { return x[x.length - 1]; }; return new Monad(tailFn); }; // 鏈式操做文件 const monad = readFile("./xxx.txt").bind(tail).bind(print); // 執行到這裏,整個操做都是純的,由於反作用函數一直被包裹在 Monad 裏,並無執行 monad.value(); // 執行反作用函數
上面代碼中,咱們將反作用函數封裝到 Monad 裏,以保證純函數的優良特性,巧妙地化解了反作用存在的安全隱患。
Ok,到這裏爲止,本文的主要內容就已經分享完了,但在學習 Monad 中的某一天,忽然發現有人用一句話就解釋清楚了 Monad,自嘆不如,簡直太厲害了,咱們一塊兒來看一下吧!
Warning:下文的內容偏數學理論,不感興趣的同窗跳過便可。
早在 10 多年前,Philip Wadler 就對 Monad 作了一句話的總結。
原文:_A monad is a monoid in the category of endofunctors_。翻譯:Monad 是一個 自函子 範疇 上的 幺半羣」 。
這裏標註了 3 個重要的概念:自函子、範疇、幺半羣,這些都是數學知識,咱們分開理解一下。
任何事物都是對象,大量的對象結合起來就造成了集合,對象和對象之間存在一個或多個聯繫,任何一個聯繫就叫作態射。
一堆對象,以及對象之間的全部態射所構成的一種代數結構,便稱之爲 範疇。
咱們將範疇與範疇之間的映射稱之爲 函子。映射是一種特殊的態射,因此函子也是一種態射。
自函子就是一個將範疇映射到自身的函子。
若是一個集合,知足結合律,那麼就是一個半羣。
單位元是集合裏的一種特別的元素,與該集合裏的二元運算有關。當單位元和其餘元素結合時,並不會改變那些元素。
如: 任何一個數 + 0 = 這個數自己。 那麼 0 就是單位元(加法單位元) 任何一個數 * 1 = 這個數自己。那麼 1 就是單位元(乘法單位元)
Ok,咱們已經瞭解了全部應該掌握的專業術語,那就簡單串解一下這段解釋吧:
一個 自函子 範疇 上的 幺半羣 ,能夠理解爲,在一個知足結合律和單位元規則的集合中,存在一個映射關係,這個映射關係能夠把集合中的元素映射成當前集合自身的元素。
相信掌握了這些理論知識,確定會對 Monad 有一個更加深刻的理解。
本文從 Monad 的維基百科開始,逐步介紹了 Monad 的內部結構以及實現原理,並經過 Promise 驗證了 Monad 在實戰中發揮的重大做用。
文中包含了許多數學定義、函數式編程的理論等知識,大可能是參考網絡資料和自我經驗得出的,若是有錯誤的地方,還望你們多多指點 🙏
最後,若是你對此有任何想法,歡迎留言評論!