重新理解 Monad
對于大多數剛剛入門函數式編程的同學來說,monad(單子、又叫單體)可能是這里面的一道坎。你可能對 map 、 flatMap 以及 filter 再熟悉不過,可是到了高階的抽象層次上就又會變得一臉懵逼。其實每個人在學習的階段都會經歷這個過程,不過希望這篇文章能讓你重新理解 monad 以及其他相關的概念。
Optional
Swift 作為一門類型安全的強類型語言,它在編譯階段就會對你的數據類型進行比較多的檢查。因此,在 Swift 中我們遇到了一種新的數據類型,叫做 Optional 。它的定義如下:
public enum Optional<Wrapped> : ExpressibleByNilLiteral {
case none
case some(Wrapped)
}
Optional 是個枚舉類型,可以看到它有兩個值: none 以及 some 。簡單點講:
值要么存在(presence),要么不存在(absence)。
這也就意味著 Optional 可能是包含了一個某個類型的值( some ),也可能是什么都沒有( none )。在這里,Optional 就是個容器( container )。
對于基礎類型,在其后面直接加上 ? 就代表了這就是個可選值類型(Optional value)。默認值即為 Optional 的預設值 nil (區別于 Objective-C)。
判斷一個可選值是否為空,我們通常會采用 if let 的寫法來解包(wrap an optional)。
if let value = value {
// if value != nil
} else {
// value = nil
}
Map
基礎類型的 map 函數調用是非常簡單的:
let ages = [20, 40, 50]
let tripledAges = ages.map { $0 * 3 }
// tripledAges = [40, 80, 100]
如果對于 Optional 呢?
let price = Optional(20)
let doubledPrice = price.map { $0 * 2 }
// doubledPrice = Optional(40)
let nilPrice: Int? = .none // equals to `nil`
let doubledNilPrice = nilPrice.map { $0 * 2 }
// doubledNilPrice = nil
我們發現, map 函數作用在 Optional 上時:
- 值存在( .some ):值類型 Optional(Int) ,返回值類型 Optional(Int) ;
- 值不存在( .none ):返回值等同輸入( nil )。
map 函數定義在 Optional 上,最大的好處就在于空值( nil )的處理,不需要我們再去使用 if let 解包(空值并沒有乘法運算):
let nilPrice: Int? = .none // equals to `nil`
var doubledNilPrice: Int?
if let nilPrice = nilPrice {
doubledNilPrice = nilPrice * 2
} else {
doubledNilPrice = nil
}
因此:
map 只對值存在的可選值進行處理。
map 通常的表示方法:
func map<U>(_ f: (T) -> U) -> Container(U)
在這里,容器 Container 就相當于 Optional ,泛型 T 和 U 均為 Int 類型。在處理完 Int 值后 map 函數就把 Int 型轉換成了 Optional(Int) ,并返回。
Async Callback Trouble
處理異步的網絡請求是一件痛苦的事情。你一定碰到過:
typealias CompletionBlock = (_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Swift.Void
// Callback
if error != nil {
/* Dealing with network errors */
}
if let json = parseToJSON(with: data) {
/* Dealing with parsing errors */
}
/* Dealing with JSON mapping errors */
/* Dealing with other errors */
/* WTF! :hankey: Sh*t! */
/* Finally, with success */
為什么我的異步回調就沒有一種方式能夠告訴我在哪里出錯了呢?方案其實也很簡單,我們先在定義一下異步處理的結果( Result ):
enum Result<T> {
case success(T)
case failure(Error)
}
有沒有發現 Result 類型很像 Optional ?沒錯。它能包含一個成功的返回值;也能在沒有返回值時提供一個錯誤消息。
我們也同時希望 map 能幫我們處理 Result :如果有結果,就從 JSON 轉換到 String 、再轉換到其他類型;否則返回錯誤信息。
這樣的 map 函數怎么寫呢?不妨先來看一下:
func map<U>(_ f: (T) -> U) -> Result<U> {
switch self {
case let .success(value):
return .success(f(value))
case let .failure(error):
return .failure(error)
}
}
舉個最基本的例子,我們希望將返回的 JSON 轉換成 String ,那在這里, map 所接受的高階變換 f 就是一個 JSON -> String 的函數。調用時, Result<JSON> 就會通過 map 最終轉換成 Result<String> 類型。
看上去很不錯!
Functor
在了解 monad 之前,我們先來了解一下它的孿生兄弟:functor(函子)。
從上面的例子中可以看到,在調用 map 函數后,我們還會把 String 類型的結果封裝成了一個可選值 Result<String> 。
像這樣能夠從容器(Container,這里即 Result )中取出元素,并通過某個函數將其轉換成可以再次被容器包裝的結果的類型就稱之為 functor。
還有些不懂?沒事,暫時就先記住有 functor 這么個玩意兒。
FlatMap
重新回到之前 JSON -> String 的例子上來。假設我們已經將某個 json 轉換成了字符串,現在需要將字符串重新格式化,那我們應該需要再調用一次 map :
func map(_ f: JSON -> Result<String>) -> Result<Result<String>>
不過我們多么希望返回的結果是個 Result<String> 的類型。不如寫一個函數來解包帶有兩層的 Result<T> 。
func flatten<T>(_ f: Result<Result<T>>) -> Result<T> {
switch f {
case let .success(value):
return value
case let .failure(error):
return .failure(error)
}
}
還有一點,在寫 flatten 函數的時候,我們也同時考慮了在 map 函數中出現轉換失敗的問題。轉換正確的時候的確我們的 map 的輸出是個 String 類型的值,隨之輸出 Result<String> 進入下一層的 map ;如果失敗,則應當是被轉換成 .failure 的結果。
將 map 和 flatten 結合一下,我們就得到了所謂的 flatMap (又稱作 bind ):
func flatMap<U>(_ f: (T) -> Result<U>) -> Result<U> {
return flatten(map(f))
}
通過 flatMap 我們可以非常輕松地處理中途出現的錯誤異常,并對給定類型進行多次連續的類型轉換。
Monad
最后再來說什么是 monad。Chris Edihof 曾在他的文章中指出:
如果可以為某個類型定義它的 flatMap 方法,那么這個類型通常就是個 monad 。(If you can define flatMap for a type, the type is often called a monad .)
在這里,我們通過 map 和 flatten 實現了 Result 類型的 flatMap 。此時,我們就可以說 Result 這個類型就是一個 monad。
Deal with Monad
到現在你就可以非常輕松地處理你的異步請求了。
func toString(_ data: Data) -> Result<String>
func toInt(_ str: String) -> Result<Int>
func toImage(_ num: Int) -> Result<UIImage>
func applyBlur(_ image: UIImage) -> Result<UIImage>
// WOW!
toString(data)
.flatMap(toInt)
.flatMap(toImage)
.flatMap(applyBlur)
Summary
-
重新回顧一下 map 和 flatMap 在 Result<T> 上的工作方式:
-
Functor、monad 可以看作是一種運算的抽象。它們的目的都是為了更好的解決類型的封裝和轉換。
Further Reading
-
ReactiveCocoa
-
Promise & Future
-
Swift 中的 throw 及 rethrow
References
- Functor and Monad in Swift - Javier Soto
- Swift 燒腦體操(五)- Monad - 唐巧
- Monads Everywhere: Porting C#’s Tasks to Swift - Nevyn Bengtsson
- Monads in Swift - Chris Edihof
- 続?ゲンバのSwift
來自:http://blog.cee.moe/recap-monad.html