翻譯連載 | 附錄 B: 謙虛的 Monad-《JavaScript輕量級函數語言程式設計》 |《你不知道的JS》姊妹篇

iKcamp發表於2019-01-29

關於譯者:這是一個流淌著滬江血液的純粹工程:認真,是 HTML 最堅實的樑柱;分享,是 CSS 裡最閃耀的一瞥;總結,是 JavaScript 中最嚴謹的邏輯。經過捶打磨練,成就了本書的中文版。本書包含了函數語言程式設計之精髓,希望可以幫助大家在學習函數語言程式設計的道路上走的更順暢。比心。

譯者團隊(排名不分先後):阿希bluekenbrucechamcfanlifedailkyoko-dfl3velilinsLittlePineappleMatildaJin冬青pobusamaCherry蘿蔔vavd317vivaxy萌萌zhouyao

JavaScript 輕量級函數語言程式設計

附錄 B: 謙虛的 Monad

首先,我坦白:在開始寫以下內容之前我並不太瞭解 Monad 是什麼。我為了確認一些事情而犯了很多錯誤。如果你不相信我,去看看 這本書 Git 倉庫 中關於本章的提交歷史吧!

我在本書中囊括了所有涉及 Monad 的話題。就像我寫書的過程一樣,每個開發者在學習函數語言程式設計的旅程中都會經歷這個部分。

儘管其他函數語言程式設計的著作差不多都把 Monad 作為開始,而我們卻只對它做了簡要說明,並基本以此結束本書。在輕量級函數語言程式設計中我確實沒有遇到太多需要仔細考慮 Monad 的問題,這就是本文更有價值的原因。但是並不是說 Monad 是沒用的或者是不普遍的 —— 恰恰相反,它很有用,也很流行。

函數語言程式設計界有一個小笑話,幾乎每個人都不得不在他們的文章或者部落格裡寫 Monad 是什麼,把它拎出來寫就像是一個儀式。在過去的幾年裡,人們把 Monad 描述為捲餅、洋蔥和各種各樣古怪的抽象概念。我肯定不會重蹈覆轍!

一個 Monad 僅僅是自函子 (endofunctor) 範疇中的一個 monoid

我們引用這句話來開場,所以把話題轉到這個引言上面似乎是很合適的。可是才不會這樣,我們不會討論 Monad 、endofunctor 或者範疇論。這句引言不僅故弄玄虛而且華而不實。

我只希望通過我們的討論,你不再害怕 Monad 這個術語或者這個概念了 —— 我曾經怕了很長一段時間 —— 並在看到該術語時知道它是什麼。你可能,也只是可能,會正確地使用到它們。

型別

在函數語言程式設計中有一個巨大的興趣領域:型別論,本書基本上完全遠離了該領域。我不會深入到型別論,坦白的說,我沒有深入的能力,即使幹了也吃力不討好。

但是我要說,Monad 基本上是一個值型別。

數字 42 有一個值型別(number),它帶有我們依賴的特徵和功能。字串 "42" 可能看起來很像,但是在程式設計裡它有不同的用途。

在物件導向程式設計中,當你有一組資料(甚至是一個單獨的離散值),並且想要給它綁上一些行為,那麼你將建立一個物件或者類來表示 "type"。接著例項就成了該型別的一員。這種做法通常被稱為 “資料結構”。

我將會非常寬泛的使用資料結構這個概念,而且我斷定,當我們在程式設計中為一個特定的值定義一組行為以及約束條件,並且將這些特徵與值一起繫結在一個單一抽象概念上時,我們可能會覺得很有用。這樣,當我們在程式設計中使用一個或多個這種值的時候,它們的行為會自然的出現,並且會使它們更方便的工作。方便的是,對你的程式碼的讀者來說,是更有描述性和宣告性的。

Monad 是一種資料結構。是一種型別。它是一組使處理某個值變得可預測的特定行為。

回顧第 8 章,我們談到了函子(functor):包括一個值和一個用來對構成函子的資料執行操作的類 map 實用函式。Monad 是一個包含一些額外行為的函子(functor)。

鬆散介面

實際上,Monad 並不是單一的資料型別,它更像是相關聯的資料型別集合。它是一種根據不同值的需要而用不同方式實現的介面。每種實現都是一種不同型別的 Monad。

例如,你可能閱讀 "Identity Monad"、"IO Monad"、"Maybe Monad"、"Either Monad" 或其他形形色色的字眼。他們中的每一個都有基本的 Monad 行為定義,但是它根據每個不同型別的 Monad 用例來繼承或者重寫互動行為。

可是它不僅僅是一個介面,因為它不只是使物件成為 Monad 的某些 API 方法的實現。對這些方法的互動的保障是必須的,是 monadic 的。這些眾所周知的常量對於使用 Monad 提高可讀性是至關重要的;另外,它是一個特殊的資料結構,讀者必須全部閱讀才能明白。

事實上,這些 Monad 方法的名字和真實介面授權的方式甚至沒有一個統一的標準;Monad 更像是一個鬆散介面。有些人稱這些方法為 bind(..),有些稱它為 chain(..),還有些稱它為 flatMap(..),等等。

所以,Monad 是一個物件資料結構,並且有充足的方法(幾乎任何名稱或排序),至少滿足了 Monad 定義的主要行為需求。每一種 Monad 都基於最少數量的方法來進行不同的擴充套件。但是,因為它們在行為上都有重疊,所以一起使用兩種不同的 Monad 仍然是直截了當和可控的。

從某種意義上說,Monad 更像是介面。

Maybe

在函數語言程式設計中,像 Maybe 這樣涵蓋 Monad 是很普遍的。事實上,Maybe Monad 是另外兩個更簡單的 Monad 的搭配:Just 和 Nothing。

既然 Monad 是一個型別,你可能認為我們應該定義 Maybe 作為一個要被例項化的類。這雖然是一種有效的方法,但是它引入了 this 繫結的問題,所以在這裡我不想討論;相反,我打算使用一個簡單的函式和物件的實現方式。

以下是 Maybe 的最簡單的實現:

var Maybe = { Just, Nothing, of/* 又稱:unit,pure */: Just };

function Just(val) {
	return { map, chain, ap, inspect };

	// *********************

	function map(fn) { return Just( fn( val ) ); }
	// 又稱:bind, flatMap
	function chain(fn) { return fn( val ); }
	function ap(anotherMonad) { return anotherMonad.map( val ); }

	function inspect() {
		return `Just(${ val })`;
	}
}

function Nothing() {
	return { map: Nothing, chain: Nothing, ap: Nothing, inspect };

	// *********************

	function inspect() {
		return "Nothing";
	}
}
複製程式碼

注意: inspect(..) 方法只用於我們的示例中。從 Monad 的角度來說,它並沒有任何意義。

如果現在大部分都沒有意義的話,不要擔心。我們將會更專注的說明我們可以用它做什麼,而不是過多的深入 Monad 背後的設計細節和理論。

所有的 Monad 一樣,任何含有 Just(..)Nothing() 的 Monad 例項都有 map(..)chain(..)(也叫 bind(..) 或者 flatMap(..))和 ap(..) 方法。這些方法及其行為的目的在於提供多個 Monad 例項一起工作的標準化方法。你將會注意到,無論 Just(..) 例項拿到的是怎樣的一個 val 值, Just(..) 例項都不會去改變它。所有的方法都會建立一個新的 Monad 例項而不是改變它。

Maybe 是這兩個 Monad 的結合。如果一個值是非空的,它是 Just(..) 的例項;如果該值是空的,它則是 Nothing() 的例項。注意,這裡由你的程式碼來決定 "空" 的意思,我們不做強制限制。下一節會詳細介紹這一點。

但是 Monad 的價值在於不論我們有 Just(..) 例項還是 Nothing() 例項,我們使用的方法都是一樣的。Nothing() 例項對所有的方法都有空操作定義。所以如果 Monad 例項出現在 Monad 操作中,它就會對 Monad 操作起短路(short-circuiting)作用。

Maybe 這個抽象概念的作用是隱式地封裝了操作和無操作的二元性。

與眾不同的 Maybe

JavaScript Maybe Monad 的許多實現都包含 nullundefined 的檢查(通常在 map(..)中),如果是空的話,就跳過該 Monad 的特性行為。事實上,Maybe 被聲稱是有價值的,因為它自動地封裝了空值檢查得以在某種程度上短路了它的特性行為。

這是 Maybe 的典型說明:

// 代替不穩定的 `console.log( someObj.something.else.entirely )`:

Maybe.of( someObj )
.map( prop( "something" ) )
.map( prop( "else" ) )
.map( prop( "entirely" ) )
.map( console.log );
複製程式碼

換句話說,如果我們在鏈式操作中的任何一環得到一個 null 或者 undefined 值,Maybe 會智慧的切換到空操作模式 —— 它現在是一個 Nothing() Monad 例項! —— 把剩餘的鏈式操作都停止掉。如果一些屬性丟失或者是空的話,巢狀的屬性訪問能安全的丟擲 JS 異常。這是非常酷的而且很實用。

但是,我們這樣實現的 Maybe 不是一個純 Monad。

Monad 的核心思想是,它必須對所有的值都是有效的,不能對值做任何檢查 —— 甚至是空值檢查。所以為了方便,這些其他的實現都是走的捷徑。這是無關緊要的。但是當學習一些東西的時候,你應該先學習它的最純粹的形式,然後再學習更復雜的規則。

我早期提供的 Maybe Monad 的實現不同於其他的 Maybe,就是它沒有空置檢查。另外,我們將 Maybe 作為 Just(..)Nothing() 的非嚴格意義上的結合。

等一下,如果我們沒有自動短路,那 Maybe 是怎麼起作用的呢?!?這似乎就是它的全部意義。

不要擔心,我們可以從外部提供簡單的空值檢查,Maybe Monad 其他的短路行為也還是可以很好的工作的。你可以在之前做一些 someObj.something.else.entirely 屬性巢狀,但是我們可以做的更 “正確”:

function isEmpty(val) {
	return val === null || val === undefined;
}

var safeProp = curry( function safeProp(prop,obj){
	if (isEmpty( obj[prop] )) return Maybe.Nothing();
	return Maybe.of( obj[prop] );
} );

Maybe.of( someObj )
.chain( safeProp( "something" ) )
.chain( safeProp( "else" ) )
.chain( safeProp( "entirely" ) )
.map( console.log );
複製程式碼

我們設計了一個用於空值檢查的 safeProp(..) 函式,並選擇了 Nothing() Monad 例項。或者把值包裝在 Just(..) 例項中(通過 Maybe.of(..))。然後我們用 chain(..) 替代 map(..),它知道如何 “展開” safeProp(..) 返回的 Monad。

當遇到空值的時候,我們得到了一連串相同的短路。只是我們把這個邏輯從 Maybe 中排除了。

不管返回哪種型別的 Monad,我們的 map(..)chain(..) 方法都有不變且可預測的反饋,這就是 Monad,尤其是 Maybe Monad 的好處。這難道不酷嗎?

Humble

現在我們對 Maybe 和它的作用有了更多的瞭解,我將會在它上面加一些小的改動 —— 我將通過設計 Maybe + Humble Monad 來新增一些轉折並且加一些詼諧的元素。從技術上來說,Humble(..) 並不是一個 Monad,而是一個產生 Maybe Monad 例項的工廠函式。

Humble 是一個使用 Maybe 來跟蹤 egoLevel 數字狀態的資料結構包裝器。具體來說,Humble(..) 只有在他們自身的水平值足夠低(少於 42)到被認為是 Humble 的時候才會執行生成的 Monad 例項;否則,它就是一個 Nothing() 空操作。這聽起來真的和 Maybe 很像!

這是一個 Maybe + Humble Monad 工廠函式:

function Humble(egoLevel) {
	// 接收任何大於等於 42 的數字
	return !(Number( egoLevel ) >= 42) ?
		Maybe.of( egoLevel ) :
		Maybe.Nothing();
}
複製程式碼

你可能會注意到,這個工廠函式有點像 safeProp(..),因為,它使用一個條件來決定是選擇 Maybe 的 Just(..) 還是 Nothing()

讓我們來看一個基礎用法的例子:

var bob = Humble( 45 );
var alice = Humble( 39 );

bob.inspect();							// Nothing
alice.inspect();						// Just(39)
複製程式碼

如果 Alice 贏得了一個大獎,現在是不是在為自己感到自豪呢?

function winAward(ego) {
	return Humble( ego + 3 );
}

alice = alice.chain( winAward );
alice.inspect();						// Nothing
複製程式碼

Humble( 39 + 3 ) 建立了一個 chain(..) 返回的 Nothing() Monad 例項,所以現在 Alice 不再有 Humble 的資格了。

現在,我們來用一些 Monad :

var bob = Humble( 41 );
var alice = Humble( 39 );

var teamMembers = curry( function teamMembers(ego1,ego2){
	console.log( `Our humble team's egos: ${ego1} ${ego2}` );
} );

bob.map( teamMembers ).ap( alice );
// Humble 佇列:41 39
複製程式碼

由於 teamMembers(..) 是柯里化的,bob.map(..) 的呼叫傳入了 bob 自身的級別(41),並且建立了一個被其餘的方法包裝的 Monad 例項。在 這個 Monad 中呼叫的 ap(alice) 呼叫了 alice.map(..),並且傳遞給來自 Monad 的函式。這樣做的效果是,Monad 的值已經提供給了 teamMembers(..) 函式,並且把顯示的結果給列印了出來。

然而,如果一個 Monad 或者兩個 Monad 實際上是 Nothing() 例項(因為它們本身的水平值太高了):

var frank = Humble( 45 );

bob.map( teamMembers ).ap( frank );

frank.map( teamMembers ).ap( bob );
複製程式碼

teamMembers(..) 永遠不會被呼叫(也沒有資訊被列印出來),因為,frank 是一個 Nothing() 例項。這就是 Maybe monad 的作用,我們的 Humble(..) 工廠函式允許我們根據自身的水平來選擇。贊!

Humility

再來一個例子來說明 Maybe + Humble 資料結構的行為:

function introduction() {
	console.log( "I'm just a learner like you! :)" );
}

var egoChange = curry( function egoChange(amount,concept,egoLevel) {
	console.log( `${amount > 0 ? "Learned" : "Shared"} ${concept}.` );
	return Humble( egoLevel + amount );
} );

var learn = egoChange( 3 );

var learner = Humble( 35 );

learner
.chain( learn( "closures" ) )
.chain( learn( "side effects" ) )
.chain( learn( "recursion" ) )
.chain( learn( "map/reduce" ) )
.map( introduction );
// 學習閉包
// 學習副作用
// 歇息遞迴
複製程式碼

不幸的是,學習過程看起來已經縮短了。我發現學習一大堆東西而不和別人分享,會使自我太膨脹,這對你的技術是不利的。

讓我們嘗試一個更好的方法:

var share = egoChange( -2 );

learner
.chain( learn( "closures" ) )
.chain( share( "closures" ) )
.chain( learn( "side effects" ) )
.chain( share( "side effects" ) )
.chain( learn( "recursion" ) )
.chain( share( "recursion" ) )
.chain( learn( "map/reduce" ) )
.chain( share( "map/reduce" ) )
.map( introduction );
// 學習閉包
// 分享閉包
// 學習副作用
// 分享副作用
// 學習遞迴
// 分享遞迴
// 學習 map/reduce
// 分享 map/reduce
// 我只是一個像你一樣的學習者 :)
複製程式碼

在學習中分享。是學習更多並且能夠學的更好的最佳方法。

總結

說了這麼多,那什麼是 Monad ?

Monad 是一個值型別,一個介面,一個有封裝行為的物件資料結構。

但是這些定義中沒有一個是有用的。這裡嘗試做一個更好的解釋:Monad 是一個用更具有宣告式的方式圍繞一個值來組織行為的方法。

和這本書中的其他部分一樣,在有用的地方使用 Monad,不要因為每個人都在函數語言程式設計中討論他們而使用他們。Monad 不是萬金油,但它確實提供了一些有用的實用函式。

**【上一章】翻譯連載 | 附錄 A:Transducing(下)-《JavaScript輕量級函數語言程式設計》 |《你不知道的JS》姊妹篇 **

翻譯連載 | 附錄 B: 謙虛的 Monad-《JavaScript輕量級函數語言程式設計》 |《你不知道的JS》姊妹篇

iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。

iKcamp官網:www.ikcamp.com 訪問官網更快閱讀全部免費分享課程: 《iKcamp出品|全網最新|微信小程式|基於最新版1.0開發者工具之初中級培訓教程分享》 《iKcamp出品|基於Koa2搭建Node.js實戰專案教程》 包含:文章、視訊、原始碼

翻譯連載 | 附錄 B: 謙虛的 Monad-《JavaScript輕量級函數語言程式設計》 |《你不知道的JS》姊妹篇


翻譯連載 | 附錄 B: 謙虛的 Monad-《JavaScript輕量級函數語言程式設計》 |《你不知道的JS》姊妹篇

2019年,iKcamp原創新書《Koa與Node.js開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!

相關文章