Swift 依然是一個有些不穩定的語言,每次發佈新版本,都帶來新的功能和特性。許多人都已經寫了 Swift 的函數的相關內容以及如何用更「純」的函數式的方法處理問題。swift
<center>
</center>api
考慮到 Swift 語言依然在初期狀態,每每在嘗試去理解一些特定的話題時,最後你會發現許多文章都是用 Swift 2.0 以前的語法,或者更糟糕的一些,內容混雜着多個版本語法。有時,搜索 flatMap
的文章,你發現不止一篇好文章都會在 Swift 中解釋 Monads。數組
相關概念缺少全面介紹的新文章,如今的許多文章經常用一些不是通俗易懂的例子和生硬的隱喻,一些人甚至使用一些難以理解的方式思考問題。閉包
在這篇簡短的文章中(這是 Swift與函數式系列文章中的一篇),我將經過引用當前庫的頭文件,試着對【如何在 Swift 2.0 中對不一樣的類型使用 map
和 flatMap
】,給出一個清晰全面的解釋。app
Map函數
map 和 flatmap 方法中, map 有着更清晰的行爲,它簡單的對輸入執行一個閉包,和 flatMap 同樣,它能夠用在 Optionals 和 SequenceTypes 上(如:數組、詞典等)。
下面是 Optionals 上 map 方法的原型:
public enum Optional<Wrapped> : ... { ... /* 若是 `self == nil` ,直接返回 `nil` 。不然返回 `f(self!)` 。 */ public func map<U>(f: (Wrapped) throws -> U) rethrows -> U? ... }
這個 map 方法指望一個簽名爲 (Wrapped) -> U
的閉包 ,若是這個可選值有值,那就解包並執行這個函數,以後再用一個可選值包裹這個結果並返回這個可選值(言外之意是指這是一個隱式可選解包,但這並無引入什麼不一樣的行爲,只須要知道 map 並無真的返回一個可選值)。
注意到輸出類型能夠和輸入類型不一樣,這就帶來了大量有用的特性。
老實說,這裏不須要多餘的解釋了,讓咱們直接看這篇文章 Playground 上的代碼吧:
var o1:Int? = nil var o1m = o1.map({$0 * 2}) o1m /* Int? 類型,結果爲 nil */ o1 = 1 o1m = o1.map({$0 * 2}) o1m /* Int? 類型,結果爲 2 */ var os1m = o1.map({ (value) -> String in String(value * 2) }) os1m /* String? 類型,結果爲 2 */ os1m = o1.map({ (value) -> String in String(value * 2) }).map({"number "+$0}) os1m /* String? 類型,結果爲 "number 2" */
若是咱們老是須要修改原始的可選值,使用 map 就能夠保留原始的值,(map 只是在可選值有值的時候才執行這個閉包,不然就只是返回 nil)。但最使人興奮的特性是咱們能夠自由的鏈接多個 map 操做,他們會有序的執行,這多虧了調用 map 老是會返回一個可選值。這樣,咱們就可以進行可選值的鏈式調用了。
可是在 SequenceTypes 上,好比數組和字典,使用 map 方法就很難跳過爲空的可選值:
var a1 = [1,2,3,4,5,6] var a1m = a1.map({$0 * 2}) a1m /* [Int] 類型,結果爲 [2, 4, 6, 8, 10, 12] */ let ao1:[Int?] = [1,2,3,4,5,6] var ao1m = ao1.map({$0! * 2}) ao1m /* [Int] 類型,結果爲 [2, 4, 6, 8, 10, 12] */ var a1ms = a1.map({ (value) -> String in String(value * 2) }).map { (stringValue) -> Int? in Int(stringValue) } a1ms /* [Int?] 類型,結果爲 [.Some(2),.Some(4),.Some(6),.Some(8),.Some(10),.Some(12)] */
這時咱們調用的 map 方法在 SequenceTypes 下定義成這個樣子:
/* 返回一個對 `self` 每一個元素進行變換後的結果數組
複雜度: O(N).
*/
map<T>(@noescape transform: (Self.Generator.Element) throws -> T) rethrows -> [T]
這個變換的閉包類型 (Self.Generator.Element) throws -> T
,會應用到集合中的每一個成員,以後用一個相同的類型打包進一個數組中。和上文可選值的例子同樣,有序的操做能夠像管道(pipeline)同樣在上一個 map 操做返回的結果上調用 map。
這些基本就是你能夠用 map
作的事情了,但在開始 flatMap
前,咱們再看三個例子:
var s1:String? = "1" var i1 = s1.map { Int($0) } i1 /* Int?? 類型,結果爲 1 */ var ar1 = ["1","2","3","a"] var ar1m = ar1.map { Int($0) } ar1m /* [Int?] 類型,結果爲 [.Some(1),.Some(2),.Some(3),nil] */ ar1m = ar1.map { Int($0) } .filter({$0 != nil}) .map {$0! * 2} ar1m /* [Int?] 類型,結果爲 [.Some(2),.Some(4),.Some(6)] */
並非每一個 String 均可以轉成 Int ,因此咱們的整數轉換閉包老是返回一個 Int? 類型 。那在第一個例子中發生了什麼?爲何返回的是 Int?? ,也就是結尾爲何是一個可選值的可選值,在執行 map 後多了一個可選包裹。解包兩次才能夠獲得真正包含的值,雖然不是什麼大問題。但當咱們須要鏈式添加 map 操做符時就會顯得很麻煩。咱們即將看到, flatMap
會幫咱們解決這個問題。
在這個數組的例子中,若是一個 String 不能轉換成 Int ,就像 ar1
的第四個值返回的就是 nil 。可是再想一下,若是咱們但願在第一個 map 操做再鏈式添加一個 map 操做,這個操做後能得到一個更短的、只有數字沒有 nil 的數組,該怎麼辦呢?
好了,咱們只須要在中間過濾出可用的元素,而且爲下一個 map 操做準備好數據流。把這些行爲嵌入到一個 map
中是否是很麻煩?咱們來看看另外一種使用 flatMap
的方法。
map
和 flatMap
的差異看起來不大,但它們是有明顯區別的。
雖然 flatMap
依然是一個相似 map 的操做,但它在 mapping 解析後額外調用了 flatten
。讓咱們用相似上一節的代碼來分析 flatMap 的功能。
這個方法的定義有一些不一樣,但功能是類似的,只是改寫了一下注釋的含義:
public enum Optional<Wrapped> : ... { ... /* 若是 `self` 是 nil ,直接返回 `nil` ,不然返回 `f(self!)` 。 */ public func flatMap<U>(f: (Wrapped) throws -> U?) rethrows -> U? ... }
就閉包而言,這裏有一個明顯的不一樣,此次 flatMap
指望一個 (Wrapped) -> U?)
閉包。
對於可選值, flatMap 對於輸入一個可選值時應用閉包返回一個可選值,以後這個結果會被壓平,也就是返回一個解包後的結果。
本質上,相比 map
, flatMap
也就是在可選值層作了一個解包。
var fo1:Int? = nil var fo1m = fo1.flatMap({$0 * 2}) fo1m /* Int? 類型,結果是 nil */ fo1 = 1 fo1m = fo1.flatMap({$0 * 2}) fo1m /* Int? 類型,結果是 2 */ var fos1m = fo1.flatMap({ (value) -> String? in String(value * 2) }) fos1m /* String? 類型,結果是 "2" */ var fs1:String? = "1" var fi1 = fs1.flatMap { Int($0) } fi1 /* Int? 類型,結果是 1 */ var fi2 = fs1.flatMap { Int($0) }.map {$0*2} fi2 /* Int? 類型,結果是 2 */
最後一段代碼包含了一個鏈式調用的例子,使用 flatMap
就不須要額外的解包。
接下來咱們再來看一看在 SequenceType 下的操做,這是一個將結果壓平的步驟。
flatten
操做只有一個對嵌套的容器進行 (拆箱)unboxing
功能。容器能夠是一個數組,一個可選值或者是其餘能包含一個的值的容器類型。考慮一個可選值包含另外一個可選值,這和咱們將在下一小節遇到的數組包含數組的狀況相似。
這個行爲附帶着單子(Monad)上的 (裝訂)bind
操做,要了解更多能夠閱讀這篇以及這篇。
SequenceType 提供了下面默認的 flatMap
實現:
/// 返回一個將變換結果鏈接起來的數組 /// /// s.flatMap(transform) /// /// 等價於 /// /// Array(s.map(transform).flatten()) /// /// - 複雜度: O(*M* + *N*), 這裏的 *M* 是指 `self` 的長度 /// *N* 是變換結果的長度 func flatMap<S : SequenceType>(transform: (Self.Generator.Element) throws -> S) rethrows -> [S.Generator.Element] /// 返回一個包含非空值的映射變換結果 /// /// - 複雜度: O(*M* + *N*), 這裏的 *M* 是指 `self` 的長度 /// *N* 是變換結果的長度 func flatMap<T>(transform: (Self.Generator.Element) throws -> T?) rethrows -> [T]
flatMap
對序列中的每一個值應用這些轉換的閉包,而後將他們打包到一個和輸入值類型相同新的數組。
這兩個註釋的閉包描述了兩個 flatMap
功能:序列壓平和可選過濾。
咱們來看看是什麼意思:
var fa1 = [1,2,3,4,5,6] var fa1m = fa1.flatMap({$0 * 2}) fa1m /*[Int] 類型,結果是 [2, 4, 6, 8, 10, 12] */ var fao1:[Int?] = [1,2,3,4,nil,6] var fao1m = fao1.flatMap({$0}) fao1m /*[Int] 類型,結果是 [1, 2, 3, 4, 6] */ var fa2 = [[1,2],[3],[4,5,6]] var fa2m = fa2.flatMap({$0}) fa2m /*[Int] 類型,結果是 [1, 2, 3, 4, 6] */
雖然第一個例子和以前使用 map
沒什麼區別,但這很清晰的讓後面兩個代碼片斷代表出它的實用性,不須要再手動的使用壓平或者過濾。
實際上,有許多使用 flatMap
的場景會提升你的代碼可讀性,而且出錯更少。
對於上一個部分最後的代碼片斷的一個例子,咱們如今可使用flatMap
改進一下代碼:
var far1 = ["1","2","3","a"] var far1m = far1.flatMap { Int($0) } far1m /* [Int] 類型,結果是 [1, 2, 3] */ far1m = far1.flatMap { Int($0) } .map {$0 * 2} far1m /* [Int] 類型,結果是 [2, 4, 6] */
在這個場景看起來只是一點點的改進,但隨着更長的鏈式,使用 flatMap
會極大的提升可讀性。
讓我重申一遍,也是在這個狀況下, Swift 中的 flatMap 行爲與 Monads 的 bind
是一致的(而且一般 "flatMap" 和 "bind" 是一個意思),你能夠從這篇以及這篇中瞭解到更多。
在這個系列的下篇文章 (SwiftGG 譯文)你能夠學到更多關於 SequenceType 和 GeneratorType 協議的知識。
譯者注:事實上從源碼理解
map
和flatMap
效果可能更好一些,推薦一篇文章:Swift 燒腦體操(四) - map 和 flatMap 。
本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg。