<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                [TOC] # Monad ## pointed functor 在繼續后面的內容之前,我得向你坦白一件事:關于我們先前創建的容器類型上的 `of` 方法,我并沒有說出它的全部實情。真實情況是,`of` 方法不是用來避免使用 `new` 關鍵字的,而是用來把值放到*默認最小化上下文*(default minimal context)中的。是的,`of` 沒有真正地取代構造器——它是一個我們稱之為 *pointed* 的重要接口的一部分。 > *pointed functor* 是實現了 `of` 方法的 functor。 這里的關鍵是把任意值丟到容器里然后開始到處使用 `map` 的能力。 ```js IO.of("tetris").map(concat(" master")); // IO("tetris master") Maybe.of(1336).map(add(1)); // Maybe(1337) Task.of([{id: 2}, {id: 3}]).map(_.prop('id')); // Task([2,3]) Either.of("The past, present and future walk into a bar...").map( concat("it was tense.") ); // Right("The past, present and future walk into a bar...it was tense.") ``` 如果你還記得,`IO` 和 `Task` 的構造器接受一個函數作為參數,而 `Maybe` 和 `Either` 的構造器可以接受任意值。實現這種接口的動機是,我們希望能有一種通用、一致的方式往 functor 里填值,而且中間不會涉及到復雜性,也不會涉及到對構造器的特定要求。“默認最小化上下文”這個術語可能不夠精確,但是卻很好地傳達了這種理念:我們希望容器類型里的任意值都能發生 `lift`,然后像所有的 functor 那樣再 `map` 出去。 有件很重要的事我必須得在這里糾正,那就是,`Left.of` 沒有任何道理可言,包括它的雙關語也是。每個 functor 都要有一種把值放進去的方式,對 `Either` 來說,它的方式就是 `new Right(x)`。我們為 `Right` 定義 `of` 的原因是,如果一個類型容器*可以* `map`,那它就*應該* `map`。看上面的例子,你應該會對 `of` 通常的工作模式有一個直觀的印象,而 `Left` 破壞了這種模式。 你可能已經聽說過 `pure`、`point`、`unit` 和 `return` 之類的函數了,它們都是 `of` 這個史上最神秘函數的不同名稱(譯者注:此處原文是“international function of mystery”,源自惡搞《007》的電影 *Austin Powers: International Man of Mystery*,中譯名《王牌大賤諜》)。`of` 將在我們開始使用 monad 的時候顯示其重要性,因為后面你會看到,手動把值放回容器是我們自己的責任。 要避免 `new` 關鍵字,可以借助一些標準的 JavaScript 技巧或者類庫達到目的。所以從這里開始,我們就利用這些技巧或類庫,像一個負責任的成年人那樣使用 `of`。我推薦使用 `folktale`、`ramda` 或 `fantasy-land` 里的 functor 實例,因為它們同時提供了正確的 `of` 方法和不依賴 `new` 的構造器。 ## 混合比喻 ![onion](https://box.kancloud.cn/00198fcd60106e43c5dfaa1141940221_368x400.png) 你看,除了太空墨西哥卷(如果你聽說過這個傳言的話)(譯者注:此處的傳言似乎是說一個叫 Chris Hadfield 的宇航員在國際空間站做墨西哥卷的事,[視頻鏈接](https://www.youtube.com/watch?v=f8-UKqGZ_hs)),monad 還被喻為洋蔥。讓我以一個常見的場景來說明這點: ```js // Support // =========================== var fs = require('fs'); // readFile :: String -> IO String var readFile = function(filename) { return new IO(function() { return fs.readFileSync(filename, 'utf-8'); }); }; // print :: String -> IO String var print = function(x) { return new IO(function() { console.log(x); return x; }); } // Example // =========================== // cat :: IO (IO String) var cat = compose(map(print), readFile); cat(".git/config") // IO(IO("[core]\nrepositoryformatversion = 0\n")) ``` 這里我們得到的是一個 `IO`,只不過它陷進了另一個 `IO`。要想使用它,我們必須這樣調用: `map(map(f))`;要想觀察它的作用,必須這樣: `unsafePerformIO().unsafePerformIO()`。 ```js // cat :: String -> IO (IO String) var cat = compose(map(print), readFile); // catFirstChar :: String -> IO (IO String) var catFirstChar = compose(map(map(head)), cat); catFirstChar(".git/config") // IO(IO("[")) ``` 盡管在應用中把這兩個作用打包在一起沒什么不好的,但總感覺像是在穿著兩套防護服工作,結果就形成一個稀奇古怪的 API。再來看另一種情況: ```js // safeProp :: Key -> {Key: a} -> Maybe a var safeProp = curry(function(x, obj) { return new Maybe(obj[x]); }); // safeHead :: [a] -> Maybe a var safeHead = safeProp(0); // firstAddressStreet :: User -> Maybe (Maybe (Maybe Street) ) var firstAddressStreet = compose( map(map(safeProp('street'))), map(safeHead), safeProp('addresses') ); firstAddressStreet( {addresses: [{street: {name: 'Mulburry', number: 8402}, postcode: "WC2N" }]} ); // Maybe(Maybe(Maybe({name: 'Mulburry', number: 8402}))) ``` 這里的 functor 同樣是嵌套的,函數中三個可能的失敗都用了 `Maybe` 做預防也很干凈整潔,但是要讓最后的調用者調用三次 `map` 才能取到值未免也太無禮了點——我們和它才剛剛見面而已。這種嵌套 functor 的模式會時不時地出現,而且是 monad 的主要使用場景。 我說過 monad 像洋蔥,那是因為當我們用 `map` 剝開嵌套的 functor 以獲取它里面的值的時候,就像剝洋蔥一樣讓人忍不住想哭。不過,我們可以擦干眼淚,做個深呼吸,然后使用一個叫作 `join` 的方法。 ```js var mmo = Maybe.of(Maybe.of("nunchucks")); // Maybe(Maybe("nunchucks")) mmo.join(); // Maybe("nunchucks") var ioio = IO.of(IO.of("pizza")); // IO(IO("pizza")) ioio.join() // IO("pizza") var ttt = Task.of(Task.of(Task.of("sewers"))); // Task(Task(Task("sewers"))); ttt.join() // Task(Task("sewers")) ``` 如果有兩層相同類型的嵌套,那么就可以用 `join` 把它們壓扁到一塊去。這種結合的能力,functor 之間的聯姻,就是 monad 之所以成為 monad 的原因。來看看它更精確的完整定義: > monad 是可以變扁(flatten)的 pointed functor。 一個 functor,只要它定義個了一個 `join` 方法和一個 `of` 方法,并遵守一些定律,那么它就是一個 monad。`join` 的實現并不太復雜,我們來為 `Maybe` 定義一個: ```js Maybe.prototype.join = function() { return this.isNothing() ? Maybe.of(null) : this.__value; } ``` 看,就像子宮里雙胞胎中的一個吃掉另一個那么簡單。如果有一個 `Maybe(Maybe(x))`,那么 `.__value` 將會移除多余的一層,然后我們就能安心地從那開始進行 `map`。要不然,我們就將會只有一個 `Maybe`,因為從一開始就沒有任何東西被 `map` 調用。 既然已經有了 `join` 方法,我們把 monad 魔法作用到 `firstAddressStreet` 例子上,看看它的實際作用: ```js // join :: Monad m => m (m a) -> m a var join = function(mma){ return mma.join(); } // firstAddressStreet :: User -> Maybe Street var firstAddressStreet = compose( join, map(safeProp('street')), join, map(safeHead), safeProp('addresses') ); firstAddressStreet( {addresses: [{street: {name: 'Mulburry', number: 8402}, postcode: "WC2N" }]} ); // Maybe({name: 'Mulburry', number: 8402}) ``` 只要遇到嵌套的 `Maybe`,就加一個 `join`,防止它們從手中溜走。我們對 `IO` 也這么做試試看,感受下這種感覺。 ```js IO.prototype.join = function() { return this.unsafePerformIO(); } ``` 同樣是簡單地移除了一層容器。注意,我們還沒有提及純粹性的問題,僅僅是移除過度緊縮的包裹中的一層而已。 ```js // log :: a -> IO a var log = function(x) { return new IO(function() { console.log(x); return x; }); } // setStyle :: Selector -> CSSProps -> IO DOM var setStyle = curry(function(sel, props) { return new IO(function() { return jQuery(sel).css(props); }); }); // getItem :: String -> IO String var getItem = function(key) { return new IO(function() { return localStorage.getItem(key); }); }; // applyPreferences :: String -> IO DOM var applyPreferences = compose( join, map(setStyle('#main')), join, map(log), map(JSON.parse), getItem ); applyPreferences('preferences').unsafePerformIO(); // Object {backgroundColor: "green"} // <div style="background-color: 'green'"/> ``` `getItem` 返回了一個 `IO String`,所以可以直接用 `map` 來解析它。`log` 和 `setStyle` 返回的都是 `IO`,所以必須要使用 `join` 來保證這里邊的嵌套處于控制之中。 ## chain 函數 (譯者注:此處標題原文是“My chain hits my chest”,是英國歌手 M.I.A 單曲 *Bad Girls* 的一句歌詞。據說這首歌有體現女權主義。) ![chain](https://box.kancloud.cn/6341f2be8b5c830a47b0cbb5b1370298_425x408.png) 你可能已經從上面的例子中注意到這種模式了:我們總是在緊跟著 `map` 的后面調用 `join`。讓我們把這個行為抽象到一個叫做 `chain` 的函數里。 ```js // chain :: Monad m => (a -> m b) -> m a -> m b var chain = curry(function(f, m){ return m.map(f).join(); // 或者 compose(join, map(f))(m) }); ``` 這里僅僅是把 map/join 套餐打包到一個單獨的函數中。如果你之前了解過 monad,那你可能已經看出來 `chain` 叫做 `>>=`(讀作 bind)或者 `flatMap`;都是同一個概念的不同名稱罷了。我個人認為 `flatMap` 是最準確的名稱,但本書還是堅持使用 `chain`,因為它是 JS 里接受程度最高的一個。我們用 `chain` 重構下上面兩個例子: ```js // map/join var firstAddressStreet = compose( join, map(safeProp('street')), join, map(safeHead), safeProp('addresses') ); // chain var firstAddressStreet = compose( chain(safeProp('street')), chain(safeHead), safeProp('addresses') ); // map/join var applyPreferences = compose( join, map(setStyle('#main')), join, map(log), map(JSON.parse), getItem ); // chain var applyPreferences = compose( chain(setStyle), chain(log), map(JSON.parse), getItem ); ``` 我把所有的 `map/join` 都替換為了 `chain`,這樣代碼就顯得整潔了些。整潔固然是好事,但 `chain` 的能力卻不止于此——它更多的是龍卷風而不是吸塵器。因為 `chain` 可以輕松地嵌套多個作用,因此我們就能以一種純函數式的方式來表示 *序列*(sequence) 和 *變量賦值*(variable assignment)。 ```js // getJSON :: Url -> Params -> Task JSON // querySelector :: Selector -> IO DOM getJSON('/authenticate', {username: 'stale', password: 'crackers'}) .chain(function(user) { return getJSON('/friends', {user_id: user.id}); }); // Task([{name: 'Seimith', id: 14}, {name: 'Ric', id: 39}]); querySelector("input.username").chain(function(uname) { return querySelector("input.email").chain(function(email) { return IO.of( "Welcome " + uname.value + " " + "prepare for spam at " + email.value ); }); }); // IO("Welcome Olivia prepare for spam at olivia@tremorcontrol.net"); Maybe.of(3).chain(function(three) { return Maybe.of(2).map(add(three)); }); // Maybe(5); Maybe.of(null).chain(safeProp('address')).chain(safeProp('street')); // Maybe(null); ``` 本來我們可以用 `compose` 寫上面的例子,但這將需要幾個幫助函數,而且這種風格怎么說都要通過閉包進行明確的變量賦值。相反,我們使用了插入式的 `chain`。順便說一下,`chain` 可以自動從任意類型的 `map` 和 `join` 衍生出來,就像這樣:`t.prototype.chain = function(f) { return this.map(f).join(); }`。如果手動定義 `chain` 能讓你覺得性能會好點的話(實際上并不會),我們也可以手動定義它,盡管還必須要費力保證函數功能的正確性——也就是說,它必須與緊接著后面有 `join` 的 `map` 相等。如果 `chain` 是簡單地通過結束調用 `of` 后把值放回容器這種方式定義的,那么就會造成一個有趣的后果,即可以從 `chain` 那里衍生出一個 `map`。同樣地,我們還可以用 `chain(id)` 定義 `join`。聽起來好像是在跟魔術師玩德州撲克,魔術師想要什么牌就有什么牌;但是就像大部分的數學理論一樣,所有這些原則性的結構都是相互關聯的。[fantasyland](https://github.com/fantasyland/fantasy-land) 倉庫中提到了許多上述衍生概念,這個倉庫也是 JavaScript 官方的代數數據結構(algebraic data types)標準。 好了,我們來看上面的例子。第一個例子中,可以看到兩個 `Task` 通過 `chain` 連接形成了一個異步操作的序列——它先獲取 `user`,然后用 `user.id` 查找 `user` 的 `friends`。`chain` 避免了 `Task(Task([Friend]))` 這種情況。 第二個例子是用 `querySelector` 查找幾個 input 然后創建一條歡迎信息。注意看我們是如何在最內層的函數里訪問 `uname` 和 `email` 的——這是函數式變量賦值的絕佳表現。因為 `IO` 大方地把它的值借給了我們,我們也要負起以同樣方式把值放回去的責任——不能辜負它的信任(還有整個程序的信任)。`IO.of` 非常適合做這件事,同時它也解釋了為何 pointed 這一特性是 monad 接口得以存在的重要前提。不過,`map` 也能返回正確的類型: ```js querySelector("input.username").chain(function(uname) { return querySelector("input.email").map(function(email) { return "Welcome " + uname.value + " prepare for spam at " + email.value; }); }); // IO("Welcome Olivia prepare for spam at olivia@tremorcontrol.net"); ``` 最后兩個例子用了 `Maybe`。因為 `chain` 其實是在底層調用了 `map`,所以如果遇到 `null`,代碼就會立刻停止運行。 如果覺得這些例子不太容易理解,你也不必擔心。多跑跑代碼,多琢磨琢磨,把代碼拆開來研究研究,再把它們拼起來看看。總之記住,返回的如果是“普通”值就用 `map`,如果是 `functor` 就用 `chain`。 這里我得提醒一下,上述方式對兩個不同類型的嵌套容器是不適用的。functor 組合,以及后面會講到的 monad transformer 可以幫助我們應對這種情況。 ## 炫耀 這種容器編程風格有時也能造成困惑,我們不得不努力理解一個值到底嵌套了幾層容器,或者需要用 `map` 還是 `chain`(很快我們就會認識更多的容器類型)。使用一些技巧,比如重寫 `inspect` 方法之類,能夠大幅提高 debug 的效率。后面我們也會學習如何創建一個“棧”,使之能夠處理任何丟給它的作用(effects)。不過,有時候也需要權衡一下是否值得這樣做。 我很樂意揮起 monad 之劍,向你展示這種編程風格的力量。就以讀一個文件,然后就把它直接上傳為例吧: ```js // readFile :: Filename -> Either String (Future Error String) // httpPost :: String -> Future Error JSON // upload :: String -> Either String (Future Error JSON) var upload = compose(map(chain(httpPost('/uploads'))), readFile); ``` 這里,代碼不止一次在不同的分支執行。從類型簽名可以看出,我們預防了三個錯誤——`readFile` 使用 `Either` 來驗證輸入(或許還有確保文件名存在);`readFile` 在讀取文件的時候可能會出錯,錯誤通過 `readFile` 的 `Future` 表示;文件上傳可能會因為各種各樣的原因出錯,錯誤通過 `httpPost` 的 `Future` 表示。我們就這么隨意地使用 `chain` 實現了兩個嵌套的、有序的異步執行動作。 所有這些操作都是在一個從左到右的線性流中完成的,是完完全全純的、聲明式的代碼,是可以等式推導(equational reasoning)并擁有可靠特性(reliable properties)的代碼。我們沒有被迫使用不必要甚至令人困惑的變量名,我們的 `upload` 函數符合通用接口而不是特定的一次性接口。這些都是在一行代碼中完成的啊! 讓我們來跟標準的命令式的實現對比一下: ```js // upload :: String -> (String -> a) -> Void var upload = function(filename, callback) { if(!filename) { throw "You need a filename!"; } else { readFile(filename, function(err, contents) { if(err) throw err; httpPost(contents, function(err, json) { if(err) throw err; callback(json); }); }); } } ``` 看看,這簡直就是魔鬼的算術(譯者注:此處原文是“the devil's arithmetic”,為美國 1988 年出版的歷史小說,講述一個猶太小女孩穿越到 1942 年的集中營的故事。此書亦有同名改編電影,中譯名《穿梭集中營》),我們就像一顆彈珠一樣在變幻莫測的迷宮中穿梭。無法想象如果這是一個典型的應用,而且一直在改變變量會怎樣——我們肯定會像陷入瀝青坑那樣無所適從。 # 理論 我們要看的第一條定律是結合律,但可能不是你熟悉的那個結合律。 ```js // 結合律 compose(join, map(join)) == compose(join, join) ``` 這些定律表明了 monad 的嵌套本質,所以結合律關心的是如何讓內層或外層的容器類型 `join`,然后取得同樣的結果。用一張圖來表示可能效果會更好: ![monad_associativity](https://box.kancloud.cn/928b1e5bc6330ef1b85b5a8f4288f420_553x209.png) 從左上角往下,先用 `join` 合并 `M(M(M a))` 最外層的兩個 `M`,然后往左,再調用一次 `join`,就得到了我們想要的 `M a`。或者,從左上角往右,先打開最外層的 `M`,用 `map(join)` 合并內層的兩個 `M`,然后再向下調用一次 `join`,也能得到 `M a`。不管是先合并內層還是先合并外層的 `M`,最后都會得到相同的 `M a`,所以這就是結合律。值得注意的一點是 `map(join) != join`。兩種方式的中間步驟可能會有不同的值,但最后一個 `join` 調用后最終結果是一樣的。 第二個定律與結合律類似: ```js // 同一律 (M a) compose(join, of) == compose(join, map(of)) == id ``` 這表明,對任意的 monad `M`,`of` 和 `join` 相當于 `id`。也可以使用 `map(of)` 由內而外實現相同效果。我們把這個定律叫做“三角同一律”(triangle identity),因為把它圖形化之后就像一個三角形: ![triangle_identity](https://box.kancloud.cn/b6863ccf9ea76aa5a4755478a50b6532_624x260.png) 如果從左上角開始往右,可以看到 `of` 的確把 `M a` 丟到另一個 `M` 容器里去了。然后再往下 `join`,就得到了 `M a`,跟一開始就調用 `id` 的結果一樣。從右上角往左,可以看到如果我們通過 `map` 進到了 `M` 里面,然后對普通值 `a` 調用 `of`,最后得到的還是 `M (M a)`;再調用一次 `join` 將會把我們帶回原點,即 `M a`。 我要說明一點,盡管這里我寫的是 `of`,實際上對任意的 monad 而言,都必須要使用明確的 `M.of`。 我已經見過這些定律了,同一律和結合律,以前就在哪兒見過...等一下,讓我想想...是的!它們是范疇遵循的定律!不過這意味著我們需要一個組合函數來給出一個完整定義。見證吧: ```js var mcompose = function(f, g) { return compose(chain(f), chain(g)); } // 左同一律 mcompose(M, f) == f // 右同一律 mcompose(f, M) == f // 結合律 mcompose(mcompose(f, g), h) == mcompose(f, mcompose(g, h)) ``` 畢竟它們是范疇學里的定律。monad 來自于一個叫 “Kleisli 范疇”的范疇,這個范疇里邊所有的對象都是 monad,所有的態射都是聯結函數(chained funtions)。我不是要在沒有提供太多解釋的情況下,拿范疇學里各式各樣的概念來取笑你。我的目的是涉及足夠多的表面知識,向你說明這中間的相關性,讓你在關注日常實用特性之余,激發起對這些定律的興趣。 ## 總結 monad 讓我們深入到嵌套的運算當中,使我們能夠在完全避免回調金字塔(pyramid of doom)情況下,為變量賦值,運行有序的作用,執行異步任務等等。當一個值被困在幾層相同類型的容器中時,monad 能夠拯救它。借助 “pointed” 這個可靠的幫手,monad 能夠借給我們從盒子中取出的值,而且知道我們會在結束使用后還給它。 是的,monad 非常強大,但我們還需要一些額外的容器函數。比如,假設我們想同時運行一個列表里的 api 調用,然后再搜集返回的結果,怎么辦?是可以使用 monad 實現這個任務,但必須要等每一個 api 完成后才能調用下一個。合并多個合法性驗證呢?我們想要的肯定是持續驗證以搜集錯誤列表,但是 monad 會在第一個 `Left` 登場的時候停掉整個演出。 下一章,我們將看到 applicative functor 如何融入這個容器世界,以及為何在很多情況下它比 monad 更好用。 [第 10 章: Applicative Functor](ch10.md) ## 練習 ```js // 練習 1 // ========== // 給定一個 user,使用 safeProp 和 map/join 或 chain 安全地獲取 sreet 的 name var safeProp = _.curry(function (x, o) { return Maybe.of(o[x]); }); var user = { id: 2, name: "albert", address: { street: { number: 22, name: 'Walnut St' } } }; var ex1 = undefined; // 練習 2 // ========== // 使用 getFile 獲取文件名并刪除目錄,所以返回值僅僅是文件,然后以純的方式打印文件 var getFile = function() { return new IO(function(){ return __filename; }); } var pureLog = function(x) { return new IO(function(){ console.log(x); return 'logged ' + x; }); } var ex2 = undefined; // 練習 3 // ========== // 使用 getPost() 然后以 post 的 id 調用 getComments() var getPost = function(i) { return new Task(function (rej, res) { setTimeout(function () { res({ id: i, title: 'Love them tasks' }); }, 300); }); } var getComments = function(i) { return new Task(function (rej, res) { setTimeout(function () { res([ {post_id: i, body: "This book should be illegal"}, {post_id: i, body: "Monads are like smelly shallots"} ]); }, 300); }); } var ex3 = undefined; // 練習 4 // ========== // 用 validateEmail、addToMailingList 和 emailBlast 實現 ex4 的類型簽名 // addToMailingList :: Email -> IO([Email]) var addToMailingList = (function(list){ return function(email) { return new IO(function(){ list.push(email); return list; }); } })([]); function emailBlast(list) { return new IO(function(){ return 'emailed: ' + list.join(','); }); } var validateEmail = function(x){ return x.match(/\S+@\S+\.\S+/) ? (new Right(x)) : (new Left('invalid email')); } // ex4 :: Email -> Either String (IO String) var ex4 = undefined; ```
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看