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

關於譯者:這是一個流淌着滬江血液的純粹工程:認真,是 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》姊妹篇

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章