Swift 燒腦體操(五)- Monad

2021-02-15 唐巧
前言

Swift 其實比 Objective-C 複雜很多,相對於出生於上世紀 80 年代的 Objective-C 來說,Swift 融入了大量新特性。這也使得我們學習掌握這門語言變得相對來說更加困難。不過一切都是值得的,Swift 相比 Objective-C,寫出來的程序更安全、更簡潔,最終能夠提高我們的工作效率和質量。

Swift 相關的學習資料已經很多,我想從另外一個角度來介紹它的一些特性,我把這個角度叫做「燒腦體操」。什麼意思呢?就是我們專門挑一些比較費腦子的語言細節來學習。通過「燒腦」地思考,來達到對 Swift 語言的更加深入的理解。

這是本體操的第五節,練習前請做好準備運動,保持頭腦清醒。


Why Monad?

因為 Monad 的定義有點複雜,我們先說為什麼要理解和學習它。業界對於 Monad 的用處有著各種爭論,特別是學術派喜歡用 Haskell 來解釋它,因為「Haskell 是純函數式程式語言」。但這往往讓問題更加複雜了——我為了理解一個概念,還需要先學習一門新語言。

所以我希望就 Swift 這門語言,分享一下理解 Monad 有什麼用。實際上,即使在 Wikipedia 上,Monad) 也沒有被強行用 Haskell 來解釋。所以我相信基於 Swift 語言,還是可以把 Monad 的概念講清楚。

在我看來,之所以有 Monad 這種結構,實際上是為了鏈式調用服務的。什麼是鏈式調用呢?我們來看看下面一段代碼:

let tq: Int? = 1tq.flatMap {    $0 * 100}.flatMap {    "image" + String($0)}.flatMap {    UIImage(named: $0)}

所以,如果一句話解釋 Monad,那就是:Monad 是一種設計模式,使得業務邏輯可以用鏈式調用的方式來書寫。

在某些情況下,鏈式調用的方式組織代碼會特別有效,比如當你的調用步驟是異步的時候,很容易寫成多層嵌套的 dispatch_async,使用 Monad 可以使得多層嵌套被展開成鏈式調用,邏輯更加清楚。除了異步調用之外,編程中涉及輸入輸出、異常處理、並發處理等情況,使用 Monad 也可以使得代碼邏輯更清晰。

基礎知識封裝過的值(wrapped value)

這個中文詞是我自己想出來的,有一些人把它叫做「上下文中的值」(value with a context),有一些人把它叫做「容器中的值」(value in a container),意思是一樣的。

什麼叫做「封裝過的值」呢?即把裸露的數據放到另一個結構中。例如:

如果你願意,你也可以自己封裝一些值,比如把網絡請求的結果和網絡異常封裝在一起,做成一個 enum (如下所示)。

enum Result<T> {    case Success(T)    case Failure(ErrorType)}

判斷一個數據類型是不是「封裝過的值」,有一個簡單的辦法:就是看這個數據類型能不能「被打開」,拿出裡面的裸露的元素。

一個字符串是不是「封裝過的值」呢?前提是你如何定義它「被打開」,如果你把它的打開定義成獲得字符串裡面的每個字符,那麼字符串也可以是一個「封裝過的值」。

flatMap

在上一篇燒腦文章中我們也提到過,要識別一個類型是不是 Monad,主要就是看它是否實現了 flatMap 方法。但是,如果你像下面這麼實現 flatMap,那也不能叫 Monad:

class TangQiao {    func flatMap() {        print("Hello world")    }}

Monad 對於 flatMap 函數有著嚴格的定義,在 Haskell 語言中,這個函數名叫 bind,但是定義是一樣的,這個函數應該:

具體在執行的時候,flatMap 會對 M 進行解包得到 C,然後調用閉包 F,傳入解包後的 C,獲得新的「封裝過的值」。

我們來看看 Optional 的 flatMap 實現,驗證一下剛剛說的邏輯。源碼地址是:https://github.com/apple/swift/blob/master/stdlib/public/core/Optional.swift。

public func flatMap<U>(@noescape f: (Wrapped) throws -> U?)        rethrows -> U? {    switch self {    case .Some(let y):        return try f(y)    case .None:        return .None    }}

Optional 的 flatMap:

作用在一個「封裝過的值」:self 身上。

接受一個閉包參數 f,這個 f 的定義是:接受解包後的值,返回一個「封裝過的值」: U? 。

在執行時,flatMap 先對 self 進行解包,代碼是 case .Some(let y)。

如果解包成功,則調用函數 f,得到一個新的「封裝過的值」,代碼是 try f(y)。

如果解包出來是 .None,則返回 .None。

設計背後的追問

flatMap 接受的這個閉包參數,直觀看起來很奇怪。接受的是解包的值,返回的又是封裝過的值,一點都沒有對稱的美!

為什麼要這麼設計?不這麼設計就不能完成鏈式調用嗎?我想了半天,答案就是一個字:懶!

為什麼這麼說呢?因為「封裝過的值」大多數時候不能直接計算,所以要計算的時候都要先解包,如果我們為了追求「對稱的美」,使得函數接受的參數和返回的值都是「封裝過的值」,當然是可以的。不過如果這麼設計的話,你就會寫大量雷同的解包代碼。程序設計的時候追求「Don’t Repeat Yourself」原則,這麼做當然是不被接受的。

Functor

剛剛我們說,在設計上為了復用代碼,我們必須保證閉包的參數是解包後的值。

那麼,同樣的道理,每次返回之前都封包一下,不一樣很重複麼?我們返回的值能不能是解包後的原始值,然後自動封裝它?

答案是可以的,但是這就不是 Monad 了,這成了 Functor 了。我們上一講提到過,Functor 中實現的 map 方法,就是一個接受解包後的值,返回結果仍然是解包後的值。為了保證鏈式調用,map 會自動把結果再封包一次。

我們再來回顧一下 map 的源碼吧:

public func map<U>(@noescape f: (Wrapped) throws -> U)        rethrows -> U? {    switch self {    case .Some(let y):        return .Some(try f(y))    case .None:        return .None    }}

在該源碼中,函數 f 在被執行完後,結果會被封包成 Optional 類型,相關代碼是:.Some(try f(y))。

所以,Optional 的 map 和 flatMap 差別真的非常非常小,就看你的閉包想不想自己返回封裝後的值了。

在具體業務中,我們也有一些實際的需求,需要我們自己控制返回封裝後的值。比如 Optional 在操作的時候,如果要返回 .None,則需要使用 flatMap,錯誤的使用了 map 函數的話,就會帶來多重嵌套 nil 的問題。比如下面這個代碼,變量 b 因為是一個兩層嵌套的 nil,所以 if let 失效了。

let tq: Int? = 1let b = tq.map { (a: Int) -> Int? in    if a % 2 == 0 {        return a    } else {        return nil    }}if let _ = b {    print("not nil")}

歸根結底,你在編程時使用 Monad 還是 Functor,取決於你的具體業務需求:

Applicative

Swift 語言中並沒有原生的 Applicative,但是 Applicative 和 Functor、Monad 算是三個形影不離的三兄弟,另外它們三者的差異都很小,所以乾脆一併介紹了。

剛剛我們討論 Functor 與 Monad 時,都是說把值放在一個容器裡面。但是我們別忘了,Swift 是函數式語言,函數是一等公民,所以,函數本身也是一種值,它也可以放到一個容器裡面,而我們要討論的 Applicative,就是一種關於「封裝過的函數」的規則。

Applicative 的定義是:使用「封裝過的函數」處理「封裝過的值」。這個「封裝過的函數」解包之後的參數類型和 Functor 的要求是一樣的。

按照這個定義,我們可以自己改造數組和 Optional,使它們成為 Applicative,以下代碼就是一個示例,來自 這裡。

extension Optional {    func apply<U>(f: (T -> U)?) -> U? {        switch f {        case .Some(let someF): return self.map(someF)        case .None: return .None        }    }}extension Array {    func apply<U>(fs: [Element -> U]) -> [U] {        var result = [U]()        for f in fs {            for element in self.map(f) {                result.append(element)            }        }        return result    }}

我們為數組和 Optional 增加了一個 apply 方法,而這個方法符合 Applicative 的定義。如果和 map 方法對比,它們的唯一差別就是閉包函數是封裝過後的了:

Monad 的應用

理論都離不開應用,否則就是「然並卵」了,講完了概念,我們來看看除了 Swift 語言中的數組和 Optional,業界還有哪些對於 Monad 的應用。

Promise

PromiseKit 是一個同時支持 Objective-C 和 Swift 的異步庫。它用 Promise 來表示一個未來將要執行的操作,使用它可以簡化我們的異步操作。因為篇幅有限,本文並不打算展開詳細介紹 Promise,我們就看一個實際的使用示例吧。

假設我們有一個業務場景,需要用戶先登錄,然後登錄成功後發API獲取數據,獲取數據後更新 UITableView 的內容,整個過程如果有錯誤,顯示相應的錯誤信息。

傳統情況下,我們需要把每個操作都封裝起來,然後我們可以選擇:

另外,以上兩種方法處理錯誤邏輯都可能會有多處,雖然我們可以把報錯也封裝成一個函數,但是在多個地方調用也不太舒服。使用 PromiseKit 之後,剛剛提到的業務場景可以用如下的示意代碼來完成:

login().then {    return API.fetchKittens()}.then { fetchedKittens in    self.kittens = fetchedKittens    self.tableView.reloadData()}.catch { error in    UIAlertView(…).show()}

另外,如果你的邏輯涉及並發,PromiseKit 也可以很好地處理,例如,你希望發兩個網絡請求,當兩個網絡請求都結束時,做相應的處理。那就可以讓 PromiseKit 的 when 方法與 then 結合工作:

let search1 = MKLocalSearch(request: rq1).promise()let search2 = MKLocalSearch(request: rq2).promise()when(search1, search2).then { response1, response2 in    //…}.catch { error in    // called if either search fails}

在 PromiseKit 的設計中,then 方法接受的閉包的類型和 flatMap 是一樣的,所以它本質上就是 flatMap。Promise 其實就是一種 Monad。

ReactiveCocoa

比起 PromiseKit,ReactiveCocoa 的名氣要大得多。最新的 ReactiveCocoa 4.0 同時支持 Objective-C 和 Swift,我們在源碼中發現了 RAC 的 SignalType 就是一個 Monad:

extension SignalType {    public func flatMap<U>(strategy: FlattenStrategy, transform: Value -> SignalProducer<U, Error>)        -> Signal<U, Error> {        return map(transform).flatten(strategy)    }    public func flatMap<U>(strategy: FlattenStrategy, transform: Value -> Signal<U, Error>)        -> Signal<U, Error> {        return map(transform).flatten(strategy)    }}

總結

我們再次總結一下 Monad、Functor、Applicative:

Monad:對一種封裝過的值,使用 flatMap 函數。

Functor:對一種封裝過的值,使用 map 函數。

Applicative:對一種封裝過的值,使用 apply 函數。

我們再對比一下flatMap、map 和 apply:

flatMap:對自己解包,然後應用到一個閉包上,這個閉包:接受一個「未封裝的值」,返回一個「封裝後的值」。

map:對自己解包,然後應用到一個閉包上,這個閉包:接受一個「未封裝的值」,返回一個「未封裝的值」。

apply:對自己解包,然後對閉包解包,解包後的閉包:接受一個「未封裝的值」,返回一個「未封裝的值」。


全文完。延伸閱讀:

Swift 燒腦體操(四) - map 和 flatMap

美女的 Swift 體操 - 高階函數

Swift 燒腦體操(二) - 函數的參數

Swift 燒腦體操(一) - Optional 的嵌套


贊助商:


點擊閱讀原文,下載有魚 App。

相關焦點

  • 2016 年,你是否打算遷移到 Swift ?
    因為一個 App 完全從 Objective-C 遷移到 Swift,從產品層面上講,它是沒有任何新產出的,所以商業上它並沒有產生新的價值,但是又要耗費大量的人力,所以在這方面,我是很謹慎的,但是這不影響我自己繼續花時間研究和學習 Swift。另外,大家對 Swift 燒腦系列有什麼意見,歡迎留言告訴我。
  • 還新增適合大孩子的燒腦遊戲
    同時,我們還發現了另一款燒腦桌遊,因為火星豬適合3-12歲的小朋友,而這一款,是我們專門為那些更大一些的小朋友,甚至成人精挑細選的桌遊,因為你們的呼聲我們一直很在意。今天介紹的這款「勝在有腦」,更是堪稱腦部體操。《最強大腦》已經流行好幾年了,每次都會讓我嘆為觀止,我等普通人連比賽規則理解起來都頗為費腦。但大腦是可以訓練的,今天這款桌遊,簡直就是最強大腦遊戲的普及版,小朋友都能玩,還能在輕鬆遊戲中,鍛鍊記憶力、觀察力、推理、協調、邏輯思維、觸覺等8種能力。
  • 燒腦遊戲 | FRAMED(致命框架)
    本公眾號致力於給喜愛燒腦、喜歡挑戰的小夥伴們分享各種燒腦題,除部分原創外,其餘內容都是經小編整理後與大家分享的,若涉及版權問題煩請聯繫我們!
  • 蘇州八家最燒腦又刺激的密室逃脫都在這裡,你的智商要充值嗎?
    最燒腦:盜墓迷城——墓道錯綜複雜,流程長,謎題多,刺激但不恐怖最燒腦:德古拉之夜——升級版機關巧妙,情節很豐富,需要爬上爬下。最燒腦:金蟬脫殼——機關很多,謎題豐富,合作完成最燒腦:國王的遊戲——五線譜知識,機關設計巧妙
  • Kotlin版圖解Functor、Applicative與Monad
    老搭檔 Maybe 是一個 monad:註: 由於 Kotlin 並不區分純函數與非純函數,因此根本不需要 IO monad。前排佔座來看 monad 展示!(Haskell 中的)monad 是實現了 Monad 類型類的數據類型。Maybe 實現了這三者,所以它是 functor、 applicative、 以及 monad。這三者有什麼區別呢?
  • 最燒腦的四款解謎遊戲,端午假宅家不無聊!
    — 遊戲推薦 —| 解謎遊戲 |可能有人要說,好不容易放個假,為什麼不推薦一些輕鬆休閒的遊戲,反而是這樣的燒腦類型
  • 燒腦大師綠色藥水答案攻略最新官方版
    遊戲介紹燒腦大師綠色藥水最新版非常有特色的故事呈現
  • 《第五人格》聯合九遊 打造淘寶燒腦互動遊戲
    同時遊戲還與九遊達成聯合宣傳合作,共同策劃並在手機淘寶製作了燒腦互動遊戲,首發當天玩家只需要在手機淘寶搜索遊戲名稱《第五人格》,即可盡情體驗來自遊戲內的神秘氛圍。淘寶搜索 互動遊戲贏取禮包作為國內領先的移動遊戲分享社區,九遊憑藉遊戲全、福利多、內容豐富備受玩家歡迎。
  • 解開燒腦的謎題!快來《Creaks》探索壁紙背後的怪奇世界
    今日推薦一款略微燒腦的平臺解謎遊戲《Creaks》遊戲簡介:《嘎吱作響(Creaks)》是《機械迷城(Machinarium)》與《銀河歷險記(Samorost)》系列以及《毛線先生(CHUCHEL)》等的開發商Amanita Design推出的一款解謎新作。
  • 燒腦戰棋來襲!《炎之軌跡》12月23日驚豔登場
    經典戰棋  策略燒腦    隨著手遊市場的發展,越來越多的同質化遊戲開始佔領市場,良莠不齊的手遊讓玩家感到十分沒勁。    戰棋遊戲之所以不溫不火的原因在於操作難度大,節奏慢,玩法偏向策略,直白的說就是燒腦。
  • 圖解 Monad
    via:http://www.ruanyifeng.com/blog/2015/07/monad.html
  • 看懂這些燒腦神片,你的智商就圓滿了
    看完《盜夢空間》後,你還數得清主角們一共經歷了幾層夢境麼?看過《地心引力》和《原始碼》之後,你的腦洞是不是直接開啟了新世界的大門,腦補停都停不下來……馬上就要到來的這個周末,就讓我們開啟腦洞之旅,讓世紀君帶你「燒腦」帶你飛~(關於《星際穿越》,世紀君以前囉嗦的不少了,沒看過的童鞋可以自行點擊「閱讀原文」繼續找虐)《盜夢空間》Inception燒腦指數 ★★★★★
  • 函數式編程進階:Monad 與 異步函數的組合
    而 Monad 的準確定義是:All told, a monad in X is just a monoid in the category of endofunctors of X, with product × replaced by composition of endofunctors and unit set by the
  • 冒險解密之作《銀河歷險記3》 畫面美解密燒腦
    玩法簡單 解密「燒腦」《銀河歷險記3》講述了一個小精靈歷經辛苦找到讓銀河系星球植物失去活力的大惡人,與他鬥智鬥勇拯救銀河系生命的故事。說到解密「燒腦」,其實並不算是特別燒腦,只能說遊戲中所有的解密元素都太自然太理所當然,沒有一樣是突兀存在的,這就讓人覺得「燒腦」。
  • 創投界的吉尼斯記錄昨夜誕生 || 創互FA 17*17燒腦飯局
    本次創互FA專場分論壇是一場50VC+300項目=一場華南界最牛逼的創投圈最豪華陣容17*17燒腦飯局【創互FA專場】項目-投資對接燒腦飯局飯局主題——垂直細分,商機發現分組:技術&智能,2B企業交易,2B企業服務,文創&動漫,新媒體,旅遊,教育,餐飲,體育運動,電商,房產服務,交通出行及後市場……直接面見投資人的17*17燒腦飯局
  • 5人本《Seven精神病院2》| 又一燒腦大作,來挑戰一下
    期待已舊的《Seven精神病院2》總算上線了,之前體驗過第一部的玩家,一定不容錯過啊,那是一個燒腦,歡脫本。
  • 【手腦速算】——全腦潛能開發的好課程!
    手腦速算故名思意,手腦並用,手腦分工,手腦協調,由手到腦,計算是先腦後手,先看腦會不會計算,不會計算在用手算。該方法通過孩子雙手高頻率的活動,促進了大腦血液的循環,刺激了大腦細胞的活躍,真正開發了孩子智力水平。手腦速算學習過程訓練了孩子的正向思維,與逆向思維,邏輯性思維,抽象性思維。
  • 逃生挑戰6漢化版v27發布—為你們呈現燒腦的挑戰體驗
    《逃生挑戰6》是一款非常好玩又燒腦的冒險解謎類遊戲,遊戲難度也不會太大,不過十分考驗玩家的觀察能力、思維能力和耐心,找線索時千萬不要著急;在你毫無頭緒的時候遊戲是允許有提示的
  • 史上最燒腦桌遊——阿瓦隆
    如果你有以上煩惱,那麼今天介紹的超級燒腦聚會遊戲「阿瓦隆」適合你。 「阿瓦隆」的背景來自於「亞瑟王傳說」,玩法以「抵抗組織」為基礎擴展出的同人系列,又稱「抵抗組織2」或「圓桌騎士」。阿瓦隆是一個難度偏高且講究策略的遊戲,建議參與者們有一定的殺人遊戲或狼人殺基礎。
  • 機械界最燒腦的玩具,你敢挑戰嗎!
    和孩子一起燒腦投入100%的興趣啟迪智慧風靡全球的風力仿生玩具升級版正版引進《大人的科學系列期刊詳細的安裝引導說明,讓你可以陪同孩子一起既燒腦又投入地樂在其中~~~