聊一聊單子(Monad)

作者:Olivier Halligon,原文鏈接,原文日期:2015-10-17
譯者:ray16897188;校對:小袋子;定稿:numbbbbb

之前的一篇文章中,我們用mapflatMap這兩種基于OptionalArray類型的方法做了很多好玩兒的事情。但你可能并沒有意識到,你已經在不自知的情況下使用了單子(單子,即 Monad:一個函數式編程的術語 - 譯者注)。那么什么是單子?

什么是函子(Functors)和單子

我們在之前的一篇文章中得知了mapflatMap對于ArrayOptional來說有著相似的作用,甚至連函數簽名都十分相似。

實際上這并不是一個特例:很多類型都有類似mapflatMap的方法,而這些方法都有那種類型的簽名。這是一種十分常見的模式,這種模式的名字叫做單子

你可能之前在網上看過單子這個術語(也可能叫做函子),還看過嘗試解釋該術語的各種比喻。但是大部分比喻都讓它更加復雜難懂。

事實上,單子和函子是非常簡單的概念。它可以最終歸結為:

一個函子是一種表示為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>。當然,這樣的類型還有很多。

實際上這些類型的方法會有其他的名字,不限于mapflatMap。例如一個Promise也是一個單子,而它的相對應的mapflatMap方法叫做then

仔細看一下Promise<T>then方法簽名,思考一下:它拿到未來返回的值T,進行處理,然后要么返回一個新類型U,要么返回一個封裝了這個新類型的、新的Promise<U>... 沒錯,我們又一次得到了相同的方法簽名,所以Promise實際上也是一個單子

有很多類型都符合單子的定義。比如ResultSignal,... 你還可以想到更多(如果需要的話你甚至可以創建你自己的單子)。

看出相似性了嗎?(為方便對比加了空格)

// 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>,等等。這會讓你的代碼看起來就像是在生產線上一樣:把一個初始值拿來,讓他經過一系列的黑盒子處理,然后得到一個最終的結果。這時你就可以說你實際上是在做函數式編程了!

下面是一個示范如何將mapflatMap的調用級聯起來去做多次轉換的例子。我們從一個字符串開始,把它按單詞分開,然后依次做如下轉換:

  1. 統計每個單詞的字符個數,做計數
  2. 把每個計數轉換成一個相對應的單詞
  3. 給每個結果加個后綴
  4. 對每個字符串結果做%轉義
  5. 把每個字符串結果轉換成一個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]

上面這段代碼可能需要你研究一會兒,嘗試去理解每一個中間階段的mapflatMap的簽名是什么,并搞清楚每一步都發生了什么事。

但無論如何,你能看出來對于描述一系列處理流程來說,這是一種很好的方式。這種方式可以被看做是一條生產線,從原材料開始,然后對它做多種轉換,最終在生產線的盡頭拿到成品

結論

盡管看起來很嚇人,但單子很簡單。

但實際上,你怎么叫它們都沒關系。只要你知道如果你想把一種封裝類型轉換成另一種,而某些類型的mapflatMap方法著實能幫到你,這就夠了。


這篇文章是"Swift編程思想"系列的后記。別擔心,我還會寫很多文章,論述 Swift 在其他應用場景下的美妙之處,不過我不會再拿這些和 ObjC 比較了(因為 Swift 真的好太多了,你現在應該完全把 ObjC 忘掉了 ??)。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容