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

關於譯者:這是一個流淌着滬江血液的純粹工程:認真,是 HTML 最堅實的樑柱;分享,是 CSS 裏最閃耀的一瞥;總結,是 JavaScript 中最嚴謹的邏輯。通過捶打磨練,成就了本書的中文版。本書包含了函數式編程之精髓,但願能夠幫助你們在學習函數式編程的道路上走的更順暢。比心。前端

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

JavaScript 輕量級函數式編程

附錄 B: 謙虛的 Monad

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

我在本書中囊括了全部涉及 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實戰項目教程》
包含:文章、視頻、源代碼

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息