(原文:Functor and Monad in Swift 作者:Javier Soto 譯者:Micheal Geng)
自從我在2013年底學習函數式編程以來,許多學術性的概念一直讓我很困擾,Functor和Monad就是其中的兩個。
本篇文章將試圖弄清它們的概念,鑒於我並非專家,因此本文將偏向實用而非理論。在網上你可以找到一些試圖解釋Monad的文章,其中的有些在比喻上做得有些過了,而本文將用示例代碼描述它的概念,希望這種方式能讓你更好的理解。
也是直到最近,我才明白Monad到底意味著什麼,接下來讓我們一起來探討Monad存在的價值,以及它如何應用到你的Swift編程中。
map函數
首先我們可以注意到在2014年的WWDC中關於Swift的介紹,其中有一個非常給力的函數map,它可以實現元素的映射,我們通過下面的例子看看:
let numbers = [1, 2, 3] let doubledNumbers = numbers.map { $0 * 2 } // doubledNumbers: 2, 4, 6
這種模式的好處是,我們可以非常清楚的表示兩個元素列表作了何種轉換(在上面這個例子中,新的元素列表中的元素比原來的元素列表中的元素增加了一倍)。我們來看看傳統的方法是怎麼實現這個功能的:
var doubledImperative: [Int] = [] for number in numbers { doubledImperative.append(number * 2) } // doubledImperative: 2, 4, 6
比較兩段代碼,它們並不簡單的是一行代碼和三行代碼的區別,前者實現起來更簡單,卻有更大的信噪比(譯者注:信噪比越大表示在混合信號中雜波越少,在這裡指能更好的表示信息)。map函數可以讓我們表達我們想要實現的目標的同時不用關心它背後發生了什麼,這可以減輕我們理解代碼的難度。
但map函數並不僅僅對數組有意義,map是一個高級函數,它可以在任何類型和方法中實現,包括一種或多種映射方式,一個或多個映射關系。
我們來看看這個關於Optional函數的例子,Optional是一種嵌套一個具體值或nil的類型:
let number = Optional(815) let transformedNumber = number.map { $0 * 2 }.map { $0 % 2 == 0 } // transformedNumber: Optional.Some(true)
在Optional中使用map函數的好處是,它將為我們自動處理空值,如果我們試圖在一個nil值處進行操作,可以先使用optional.map申請轉換,如果原來為空的話,最終也將為空,這就可以避免使用if let嵌套打開Optional。
let nilNumber: Int? = .None let transformedNilNumber = nilNumber.map { $0 * 2 }.map { $0 % 2 == 0 } // transformedNilNumber: None
由此我們可以推斷,map函數在針對不同的類型時可以有不同的行為,這主要取決於該類型的語義。例如,它使Optional在值為空的時候也可以通過映射轉換使正確執行。
下面的例子是一個一般的針對Container的map方法,封裝T的值:
func map< U>(transformFunction: T -> U) -> Container< U>
讓我們對上面的代碼進行分析,T是當前的元素類型,U是將要返回的元素類型。通過這種方法我們可以實現諸如將一個字符串數組的每個字符串長度映射到一個整型數組中。
如果我們提供了一個函數,它傳入一個T類型的值,並輸出一個U類型的值,map函數將會使用這個函數來創建另一個Container的實例,其中的原始值將被transformfunction的返回值所取代。
用自己寫的類型實現map函數
讓我們自己來定義一個類型。如今的很多開源Swift項目中你可以看到Result枚舉的模式,當使用它來代替Objective-C中舊的NSError參數時,會給API帶來了一些好處。
我們可以像下面這樣定義:
class Box< T> { let unbox: T init(_ value: T) { self.unbox = value } } enum Result< T> { case Value(Box< T>) case Error(NSError) }
該Box類用來繞過當前Swift版本的一處限制(unimplemented IR generation feature non-fixed multi-payload enum layout)。
這是一種在一些語言中被稱為Either的實現模式,只有在這種情況下,我們必須使用一個NSError類來代替它,因為我們需要用它來報告我們的操作結果。
從概念上來講,Result與Optional是非常相似的:可以適用於任意類型的值,無論它是否有意義,然而在這種情況下,Result可以告訴我們無意義的值是什麼,且為什麼存在。
看下面的例子,讀取一個文件的內容,作為Result對象的返回結果:
func dataWithContentsOfFile(file: String, encoding: NSStringEncoding) -> Result { var error: NSError? if let data = NSData(contentsOfFile: file, options: .allZeros, error: &error) { return .Value(Box(data)) } else { return .Error(error!) } }
很顯然,這個函數將返回一個NSData對象,或一個NSError告知文件無法讀取。
如果在以前,我們可能為了讀出這些值,需要做一些轉換。並且需要檢測每一步轉換的值是否正確,這可能會導致我們需要使用一些繁瑣的if let 或switch嵌套來檢測。在這種情況下,我們只需要提供轉換方法,如果不這麼做,我們也可以傳遞相同的error。
假設我們要讀取一個字符串的內容,我們會得到一個NSData,然後我們需要轉化成一個字符串,之後我們將它變成大寫:
NSData -> String -> String
我們也可以通過map函數對它進行轉化:
let data: Result< NSData> = dataWithContentsOfFile(path, NSUTF8StringEncoding) let uppercaseContents: Result = data.map { NSString(data: $0, encoding: NSUTF8StringEncoding)! }.map { $0.uppercaseString }
這類似於上面使用map函數處理數組的例子,我們只需要描述清楚想要完成的目標即可。
相比之下,下面這份代碼是不使用map函數:
let data: Result< NSData> = dataWithContentsOfFile(path, NSUTF8StringEncoding) var stringContents: String? switch data { case let .Value(value): stringContents = NSString(data: value.unbox, encoding: NSUTF8StringEncoding) case let .Error(error): break } let uppercaseContents: String? = stringContents?.uppercaseString
Result.map函數背後具體做了什麼?我們看看:
extension Result { func map< U>(f: T -> U) -> Result< U> { switch self { case let .Value(value): return Result< U>.Value(Box(f(value.unbox))) case let .Error(error): return Result< U>.Error(error) } } }
就像之前的一樣,使用變換函數f輸入類型為T的值(在上面的例子中是NSData)並返回一個類型為U的值(字符串),調用map函數之後,我們會得到一個輸入類型為T,輸出類型為U的結果,每當我們有一個新的初始值我們就需要調用一次f函數,否則將會報錯。
仿函數(Functors)
我們可以看到,map實際上是實現了一個類,就像Optional、Array或Result。綜上所述,我們可以得到一個使用上像函數的一個類,使這個類有了類似於函數的行為,而這就是仿函數。
一旦你知道了仿函數是什麼,我們就可以討論一些諸如dictionary之類的東西了,並且它們就是仿函數,你會意識到你也可以試著實現仿函數的代碼了。
Monad模式
在前面的例子中,我們使用轉換函數返回另一個值,但是如果我們想使用它返回一個結果對象會怎麼樣?換句話說,如果轉換操作,我們通過map函數是否會失敗與錯誤呢?讓我們看看下面的例子:
func map< U>(f: T -> U) -> Result< U>
在我們的例子中,類型T的NSData數據我們希望轉換成類型U 。就如同下面的代碼:
func map(f: NSData -> Result< String>) -> Result< Result< String>>
注意嵌套結果的返回類型,這可能並不是我們想要的,但卻是正確的。我們可以設計一個函數使它變成我們想要的結果:
extension Result { static func flatten< T>(result: Result< Result< T>>) -> Result< T> { switch result { case let .Value(innerResult): return innerResult.unbox case let .Error(error): return Result< T>.Error(error) } } }
這個flatten函數嵌套進了類型為T的返回值內,通過提取內部對象的值返回一個類型為T的結果,或返回一個錯誤。
一個flatten函數可能實現在其他不同的環境下,例如可是設計一個flatten函數實現將一組數組組合成一維連續數組。
像這樣,我們可以實現通過map函數和flatten函數對
let stringResult = Result< String>.flatten(data.map { (data: NSData) -> (Result< String>) in if let string = NSString(data: data, encoding: NSUTF8StringEncoding) { return Result.Value(Box(string)) } else { return Result< String>.Error(NSError(domain: "com.javisoto.es.error_domain", code: JSErrorCodeInvalidStringData, userInfo: nil)) } })
這很常見,在很多地方你都會發現諸如flatmap或flattenmap函數的定義:
extension Result { func flatMap< U>(f: T -> Result< U>) -> Result< U> { return Result.flatten(map(f)) } }
於是,我們把這樣的類型定義為Monad模式,一個Monad模式就是一個仿函數,像這樣用類似於我們看到的方法實現一個功能有時也被稱為綁定。我們這裡介紹的是一些類型一樣的Monad模型,但是你也會發現諸如封裝延遲計算之類的模型,如Signal或Future。
仿函數和Monad模型來自於范疇理論,而我並不完全熟悉,然而對這些名詞進行比喻引用我認為是有價值的。計算機科學家們喜歡想一些名詞來命名這些概念,有了這些名詞可以讓我們把抽象的概念更好的理解。我們通過引用比喻可以對這些抽象的概念進行更大眾化的解釋。
我在這篇博客的寫作中花費了很長時間。如果你不熟悉這一切,我並不希望你讀完這立刻理解它。然而我鼓勵你創建一份Xcode代碼來嘗試這些例程。
下次當你聽到諸如仿函數或Monad模式時,別害怕,這次名詞被設計出來只是為了描述一些我們平常中常見的操作模式而已。
(本文為CocoaChina組織翻譯,本譯文權利歸譯者所有,未經允許禁止轉載。)