作者:Olivier Halligon,原文鏈接,原文日期:2015-10-17
譯者:ray16897188;校對:小袋子;定稿:numbbbbb
之前的一篇文章中,我們用map
、flatMap
這兩種基于Optional
和Array
類型的方法做了很多好玩兒的事情。但你可能并沒有意識到,你已經在不自知的情況下使用了單子(單子,即 Monad:一個函數式編程的術語 - 譯者注)。那么什么是單子?
什么是函子(Functors)和單子
我們在之前的一篇文章中得知了map
和flatMap
對于Array
和Optional
來說有著相似的作用,甚至連函數簽名都十分相似。
實際上這并不是一個特例:很多類型都有類似map
和flatMap
的方法,而這些方法都有那種類型的簽名。這是一種十分常見的模式,這種模式的名字叫做單子。
你可能之前在網上看過單子這個術語(也可能叫做函子),還看過嘗試解釋該術語的各種比喻。但是大部分比喻都讓它更加復雜難懂。
事實上,單子和函子是非常簡單的概念。它可以最終歸結為:
一個函子是一種表示為Type<T>
的類型,它:
- 封裝了另一種類型(類似于封裝了某個
T
類型的Array<T>
或Optional<T>
) - 有一個具有
(T->U) -> Type<U>
簽名的map
方法
一個單子是一種類型,它:
- 是一個函子(所以它封裝了一個
T
類型,擁有一個map
方法) - 還有一個具有
(T->Type<U>) -> Type<U>
簽名的flatMap
方法
這就是對單子和函子所需要了解的一切!一個單子就是一種帶有flatMap
方法的類型,一個函子就是一種帶有一個map
方法的類型。**很簡單,不是么?
各種類型的單子
你已經學過兩種既是函子又是單子的類型,它們是:Array<T>
和Optional<T>
。當然,這樣的類型還有很多。
實際上這些類型的方法會有其他的名字,不限于map
和flatMap
。例如一個Promise也是一個單子,而它的相對應的map
和flatMap
方法叫做then
。
仔細看一下Promise<T>
的then
方法簽名,思考一下:它拿到未來返回的值T
,進行處理,然后要么返回一個新類型U
,要么返回一個封裝了這個新類型的、新的Promise<U>
... 沒錯,我們又一次得到了相同的方法簽名,所以Promise
實際上也是一個單子
!
有很多類型都符合單子的定義。比如Result
,Signal
,... 你還可以想到更多(如果需要的話你甚至可以創建你自己的單子)。
看出相似性了嗎?(為方便對比加了空格)
// Array, Optional, Promise, Result 都是函子
anArray .map( transform: T -> U ) -> Array<U>
anOptional .map( transform: T -> U ) -> Optional<U>
aPromise .then( transform: T -> U ) -> Promise<U>
aResult .map( transform: T -> U ) -> Result<U>
// Array, Optional, Promise, Result 都是單子
anArray .flatMap( transform: T -> Array<U> ) -> Array<U>
anOptional .flatMap( transform: T -> Optional<U> ) -> Optional<U>
aPromise .then( transform: T -> Promise<U> ) -> Promise<U>
aResult .flatMap( transform: T -> Result<U> ) -> Result<U>
把map()
和flatMap()
級聯起來
通常你還可以把這兩個方法級聯,這會使它們更加強大。例如,最開始你有一個Array<T>
,通過使用map
來對它做轉換
操作,得到一個Array<U>
,然后對這個Array<U>
再級聯上一個map
,對它做另一個轉換
操作將其轉換成一個Array<Z>
,等等。這會讓你的代碼看起來就像是在生產線上一樣:把一個初始值拿來,讓他經過一系列的黑盒子處理,然后得到一個最終的結果。這時你就可以說你實際上是在做函數式編程了!
下面是一個示范如何將map
和flatMap
的調用級聯起來去做多次轉換的例子。我們從一個字符串開始,把它按單詞分開,然后依次做如下轉換:
- 統計每個單詞的字符個數,做計數
- 把每個計數轉換成一個相對應的單詞
- 給每個結果加個后綴
- 對每個字符串結果做%轉義
- 把每個字符串結果轉換成一個
NSURL
let formatter = NSNumberFormatter()
formatter.numberStyle = .SpellOutStyle
let string = "This is Functional Programming"
let translateURLs = string
// Split the characters into words
.characters.split(" ")
// Count the number of characters on each word
.map { $0.count }
// Spell out this number of chars (`stringFromNumber` can return nil)
.flatMap { (n: Int) -> String? in formatter.stringFromNumber(n) }
// add " letters" suffix
.map { "\($0) letters" }
// encode the string so it can be used in an NSURL framgment after the # (the stringByAdding… method can return nil)
.flatMap { $0.stringByAddingPercentEncodingWithAllowedCharacters(.URLFragmentAllowedCharacterSet()) }
// Build an NSURL using that string (`NSURL(string: …)` is failable: it can return nil)
.flatMap { NSURL(string: "https://translate.google.com/#auto/fr/\($0)") }
print(translateURLs)
// [https://translate.google.com/#auto/fr/four%20letters, https://translate.google.com/#auto/fr/two%20letters, https://translate.google.com/#auto/fr/ten%20letters, https://translate.google.com/#auto/fr/eleven%20letters]
上面這段代碼可能需要你研究一會兒,嘗試去理解每一個中間階段的map
和flatMap
的簽名是什么,并搞清楚每一步都發生了什么事。
但無論如何,你能看出來對于描述一系列處理流程來說,這是一種很好的方式。這種方式可以被看做是一條生產線,從原材料
開始,然后對它做多種轉換
,最終在生產線的盡頭拿到成品
。
結論
盡管看起來很嚇人,但單子很簡單。
但實際上,你怎么叫它們都沒關系。只要你知道如果你想把一種封裝類型轉換成另一種,而某些類型的map
和flatMap
方法著實能幫到你,這就夠了。
這篇文章是"Swift編程思想"系列的后記。別擔心,我還會寫很多文章,論述 Swift 在其他應用場景下的美妙之處,不過我不會再拿這些和 ObjC 比較了(因為 Swift 真的好太多了,你現在應該完全把 ObjC 忘掉了 ??)。