Swift 其實比 Objective-C 複雜很多,相對於出生於上世紀 80 年代的 Objective-C 來說,Swift 融入了大量新特性。這也使得我們學習掌握這門語言變得相對來說更加困難。不過一切都是值得的,Swift 相比 Objective-C,寫出來的程序更安全、更簡潔,最終能夠提高我們的工作效率和質量。
Swift 相關的學習資料已經很多,我想從另外一個角度來介紹它的一些特性,我把這個角度叫做「燒腦體操」。什麼意思呢?就是我們專門挑一些比較費腦子的語言細節來學習。通過「燒腦」地思考,來達到對 Swift 語言的更加深入的理解。
這是本體操的第五節,練習前請做好準備運動,保持頭腦清醒。
因為 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,取決於你的具體業務需求:
ApplicativeSwift 語言中並沒有原生的 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 的應用。
PromisePromiseKit 是一個同時支持 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。