我以前一直覺得我是懂 map
和 flatMap
的。可是直到我看到別人說:「一個實現了 flatMap
方法的類型其實就是 monad。」我又發現這個熟悉的東西變得陌生起來,本節燒腦體操打算更細緻一些介紹 map
和 flatMap
,爲了下一節介紹 monad 作鋪墊。git
map
和 flatMap
數組中的 map
對數組元素進行某種規則的轉換,例如:github
let arr = [1, 2, 4] // arr = [1, 2, 4] let brr = arr.map { "No." + String($0) } // brr = ["No.1", "No.2", "No.4"]
而 flatMap
和 map
的差異在哪裏呢?咱們能夠對比一下它們的定義。爲了方便閱讀,我在刪掉了定義中的 @noescape
、throws
和 rethrows
關鍵字,若是你對這些關鍵字有疑問,能夠查閱上一期的燒腦文章:面試
extension SequenceType { public func map<T>(transform: (Self.Generator.Element) -> T) -> [T] } extension SequenceType { public func flatMap<S : SequenceType>(transform: (Self.Generator.Element) -> S) -> [S.Generator.Element] } extension SequenceType { public func flatMap<T>(transform: (Self.Generator.Element) -> T?) -> [T] }
咱們從中能夠發現,map
的定義只有一個,而 flatMap
的定義有兩個重載的函數,這兩個重載的函數都是接受一個閉包做爲參數,返回一個數組。可是差異在於,閉包的定義不同。swift
第一個函數閉包的定義是:(Self.Generator.Element) -> S
,而且這裏 S 被定義成:S : SequenceType
。因此它是接受數組元素,而後輸出一個 SequenceType
類型的元素的閉包。有趣的是, flatMap
最終執行的結果並非 SequenceType
的數組,而是 SequenceType
內部元素另外組成的數組,即:[S.Generator.Element]
。數組
是否是有點暈?看看示例代碼就比較清楚了:緩存
let arr = [[1, 2, 3], [6, 5, 4]] let brr = arr.flatMap { $0 } // brr = [1, 2, 3, 6, 5, 4]
你看出來了嗎?在這個例子中,數組 arr 調用 flatMap
時,元素[1, 2, 3]
和 [6, 5, 4]
分別被傳入閉包中,又直接被做爲結果返回。可是,最終的結果中,倒是由這兩個數組中的元素共同組成的新數組:[1, 2, 3, 6, 5, 4]
。網絡
須要注意的是,其實整個 flatMap
方法能夠拆解成兩步:閉包
map
方法那樣,對元素進行某種規則的轉換。flatten
方法,將數組中的元素一一取出來,組成一個新數組。因此,剛剛的代碼其實等價於:app
let arr = [[1, 2, 3], [6, 5, 4]] let crr = Array(arr.map{ $0 }.flatten()) // crr = [1, 2, 3, 6, 5, 4]
講完了 flatMap
的第一種重載的函數,咱們再來看第二種重載。函數
在第二種重載中,閉包的定義變成了:(Self.Generator.Element) -> T?
,返回值 T 再也不像第一種重載中那樣要求是數組了,而變成了一個 Optional 的任意類型。而 flatMap
最終輸出的數組結果,其實不是這個 T?
類型,而是這個 T?
類型解包以後,不爲 .None
的元數數組:[T]
。
咱們仍是直接看代碼吧。
let arr: [Int?] = [1, 2, nil, 4, nil, 5] let brr = arr.flatMap { $0 } // brr = [1, 2, 4, 5]
在這個例子中,flatMap
將數組中的 nil 都丟棄掉了,只保留了非空的值。
在實際業務中,這樣的例子還挺常見,好比你想構造一組圖片,因而你使用 UIImage 的構造函數,可是這個函數可能會失敗(好比圖像的名字不存在時),因此返回的是一個 Optional 的 UIImage 對象。使用 flatMap
方法能夠方便地將這些對象中爲 .None 的都去除掉。以下所示:
let images = (1...6).flatMap { UIImage(named: "imageName-\($0)") }
map
和 flatMap
其實 map
和 flatMap
不止存在於數組中,在 Optional 中也存在。咱們先看看定義吧:
public enum Optional<Wrapped> : _Reflectable, NilLiteralConvertible { case None case Some(Wrapped) public func map<U>( f: (Wrapped) throws -> U) rethrows -> U? public func flatMap<U>( f: (Wrapped) throws -> U?) rethrows -> U? }
因此,對於一個 Optional 的變量來講,map
方法容許它再次修改本身的值,而且沒必要關心本身是否爲 .None
。例如:
let a1: Int? = 3 let b1 = a1.map{ $0 * 2 } // b1 = 6 let a2: Int? = nil let b2 = a2.map{ $0 * 2 } // b2 = nil
再舉一個例子,好比咱們想把一個字符串轉成 NSDate 實例,若是不用 map
方法,咱們只能這麼寫:
var date: NSDate? = ... var formatted = date == nil ? nil : NSDateFormatter().stringFromDate(date!)
而使用 map
函數後,代碼變得更短,更易讀:
var date: NSDate? = ... var formatted = date.map(NSDateFormatter().stringFromDate)
看出來特色了嗎?當咱們的輸入是一個 Optional,同時咱們須要在邏輯中處理這個 Optional 是否爲 nil,那麼就適合用 map
來替代原來的寫法,使得代碼更加簡短。
那何時使用 Optional 的 flatMap
方法呢?答案是:當咱們的閉包參數有可能返回 nil 的時候。
好比,咱們但願將一個字符串轉換成 Int,可是轉換可能失敗,這個時候咱們就能夠用 flatMap
方法,以下所示:
let s: String? = "abc" let v = s.flatMap { (a: String) -> Int? in return Int(a) }
我在這裏還發現了更多的使用 map
和 flatMap
的例子,分享給你們:http://blog.xebia.com/the-power-of-map-and-flatmap-of-swift-optionals/。
map
和 flatMap
的源碼Talk is cheap. Show me the code.
-- Linus Torvalds
爲了更好地理解,咱們去翻翻蘋果開源的 Swift 代碼,看看 map
和 flatMap
的實現吧。
map
的源碼源碼地址是:https://github.com/apple/swift/blob/master/stdlib/public/core/Collection.swift,摘錄以下:
public func map<T>(@noescape transform: (Generator.Element) throws -> T) rethrows -> [T] { let count: Int = numericCast(self.count) if count == 0 { return [] } var result = ContiguousArray<T>() result.reserveCapacity(count) var i = self.startIndex for _ in 0..<count { result.append(try transform(self[i])) i = i.successor() } _expectEnd(i, self) return Array(result) }
flatMap
的源碼(重載函數一)剛剛也說到,數組的 flatMap
有兩個重載的函數。咱們先看第一個的函數實現。源碼地址是:https://github.com/apple/swift/blob/master/stdlib/public/core/SequenceAlgorithms.swift.gyb。
public func flatMap<S : SequenceType>( transform: (${GElement}) throws -> S ) rethrows -> [S.${GElement}] { var result: [S.${GElement}] = [] for element in self { result.appendContentsOf(try transform(element)) } return result }
對於這個代碼,咱們能夠看出,它作了如下幾件事情:
result
的新數組,用於存放結果。transform
,進行轉換。appendContentsOf
方法,將結果放入 result
數組中。而這個 appendContentsOf
方法,便是把數組中的元素取出來,放入新數組。如下是一個簡單示例:
var arr = [1, 3, 2] arr.appendContentsOf([4, 5]) // arr = [1, 3, 2, 4, 5]
因此這種 flatMap
必需要求 transform
函數返回的是一個 SequenceType
類型,由於appendContentsOf
方法須要的是一個 SequenceType
類型的參數。
flatMap
的源碼(重載函數二)當咱們的閉包參數返回的類型不是 SequenceType
時,就會匹配上第二個重載的 flatMap
函數。如下是函數的源碼。
public func flatMap<T>( @noescape transform: (${GElement}) throws -> T? ) rethrows -> [T] { var result: [T] = [] for element in self { if let newElement = try transform(element) { result.append(newElement) } } return result }
咱們也用一樣的方式,把該函數的邏輯理一下:
result
的新數組,用於存放結果。(和另外一個重載函數徹底同樣)transform
,進行轉換。(和另外一個重載函數徹底同樣)append
方法,將結果放入result
數組中。(惟一差異的地方)因此,該 flatMap
函數能夠過濾閉包執行結果爲 nil 的狀況,僅收集那些轉換後非空的結果。
對於這種重載的 flatMap
函數,它和 map
函數的邏輯很是類似,僅僅多作了一個判斷是否爲 nil 的邏輯。
因此,面試題來了:「什麼狀況下數組的 map
能夠和 flatMap
等價替換?」
答案是:當 map
的閉包函數返回的結果不是 SequenceType
的時候。由於這樣的話,flatMap
就會調到咱們當前討論的這種重載形式。而這種重載形式和 map
的差別就僅僅在於要不要判斷結果爲 nil。
下面是一個示例代碼,能夠看出:brr
和 crr
雖然分別使用 map
和 flatMap
生成,可是結果徹底同樣:
let arr = [1, 2, 4] // arr = [1, 2, 4] let brr = arr.map { "No." + String($0) } // brr = ["No.1", "No.2", "No.4"] let crr = arr.flatMap { "No." + String($0) } // crr = ["No.1", "No.2", "No.4"]
map
和 flatMap
源碼看完數組的實現,咱們再來看看 Optional 中的相關實現。源碼地址是:https://github.com/apple/swift/blob/master/stdlib/public/core/Optional.swift,摘錄以下:
/// If `self == nil`, returns `nil`. /// Otherwise, returns `f(self!)`. public func map<U>(@noescape f: (Wrapped) throws -> U) rethrows -> U? { switch self { case .Some(let y): return .Some(try f(y)) case .None: return .None } } /// Returns `nil` if `self` is `nil`, /// `f(self!)` otherwise. @warn_unused_result public func flatMap<U>(@noescape f: (Wrapped) throws -> U?) rethrows -> U? { switch self { case .Some(let y): return try f(y) case .None: return .None } }
Optional 的這兩函數真的是驚人的類似,若是你只看兩段函數的註釋的話,甚至看不出這兩個函數的差異。
這兩函數實現的差異僅僅只有兩處:
兩個函數最終都保證了返回結果是 Optional 的。只是將結果轉換成 Optional 的位置不同。
這就像我老婆給我說:「我喜歡這個東西,你送給我嗎?不送的話我就直接刷你卡買了!」。。。買東西的結果本質上是同樣的,誰付錢本質上也是同樣的,差異只是誰動手而已。
既然 Optional 的 map
和 flatMap
本質上是同樣的,爲何要搞兩種形式呢?這實際上是爲了調用者更方便而設計的。調用者提拱的閉包函數,既能夠返回 Optional 的結果,也能夠返回非 Optional 的結果。對於後者,使用 map
方法,便可以將結果繼續轉換成 Optional 的。結果是 Optional 的意味着咱們能夠繼續鏈式調用,也更方便咱們處理錯誤。
咱們來看一段略燒腦的代碼,它使用了 Optional 的 flatMap
方法:
var arr = [1, 2, 4] let res = arr.first.flatMap { arr.reduce($0, combine: max) }
這段代碼的功能是:計算出數組中的元素最大值,按理說,求最大值直接使用 reduce 方法就能夠了。不過有一種特殊狀況須要考慮:即數組中的元素個數爲 0 的狀況,在這種狀況下,沒有最大值。
咱們使用 Optional 的 flatMap
方法來處理了這種狀況。arr 的 first
方法返回的結果是 Optional 的,當數組爲空的時候,first
方法返回 .None,因此,這段代碼能夠處理數組元素個數爲 0 的狀況了。
map
和 flatMap
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
有一位大師說,計算機世界真正稱得上難題的就只有兩個:第一個是緩存過時問題,第二個就是取名字。做爲文章最後的燒腦環節,咱們來聊聊取名字這個事吧。
我來提幾個看起來「無厘頭」的問題:
map
函數和 Optinal 的 map
函數的實現差異巨大?可是爲何都叫 map
這個名字?flatMap
函數和 Optinal 的 flatMap
函數的實現差異巨大?可是爲何都叫flatMap
這個名字?flatMap
有兩個重載的函數,兩個重載的函數差異巨大,可是爲何都叫 flatMap
這個名字?在我看來,這樣的取名其實都是有背後的緣由的,我試着分享一下個人理解。咱們先說結論,而後再解釋。這段結論來自:http://www.mokacoding.com/blog/functor-applicative-monads-in-pictures/。
好吧,我猜你內心開始罵娘了:「爲了解釋一個問題,引入了兩個新問題:誰知道什麼是 Functor 和 Monad !」
不要着急,咱們先說嚴謹的結論有助於更好地總結和概括,我下面試着解釋一下 Functor 和 Monad。
Functor 在 Wikipedia 上的定義很是學術。我想了一個相對比較容易理解的定義:所謂的 Functor,就是能夠把一個函數應用於一個「封裝過的值」上,獲得一個新的「封裝過的值」。一般狀況下,咱們會把這個函數叫作 map
。
什麼叫作「封裝過的值」呢?數組就是對值的一種封裝,Optional 也是對值的一種封裝。若是你願意,你也能夠本身封裝一些值,好比把網絡請求的結果和網絡異常封裝在一塊兒,作成一個 enum (以下所示)。
enum Result<T> { case Success(T) case Failure(ErrorType) }
一個值可否成爲「封裝過的值」,取決於這個值的類型所表示的集合,經過 map
函數,可否映射到一個新集合中。這個新集合,也要求可以繼續使用 map
函數,再映射到另一個集合。
咱們拿數組和 Optional 類型來檢查這個規則,就會發現是符合的:
map
函數,生成一個新的數組,新的數組能夠繼續使用 map
函數。map
函數,生成一個新的 Optional 變量,新的 Optional 變量能夠繼續使用map
函數。因此,數組 和 Optional 都是 Functor。
若是你能理解 Functor,那麼 Monad 就相對容易一些了。所謂的 Monad,和 Functor 同樣,也是把一個函數應用於一個「封裝過的值」上,獲得一個新的「封裝過的值」。不過差異在於:
下面我舉例解釋一下:
剛剛咱們說,數組 和 Optional 都是 Functor,由於它們支持用 map
函數作「封裝過的值」所在集合的變換。那麼,你注意到了嗎?map 函數的定義中,輸入的參數和返回的結果,都不是「封裝過的值」,而是「未封裝的值」。什麼是「未封裝的值」?
下面是數組的示例代碼,我故意加上了閉包的參數,咱們再觀察一下。咱們能夠發現,map
的閉包接受的是 Int 類型,返回的是 String 類型,都是一個一個的元素類型,而不是數組。
// map 的閉包接受的是 Int 類型,返回的是 String 類型,都是一個一個的元素類型,而不是數組。 let arr = [1, 2, 4] let brr = arr.map { (element: Int) -> String in "No." + String(element) }
下面是 Optional 的示例代碼,我也故意加上了閉包的參數。咱們能夠發現,map
的閉包接受的是 Int 類型,返回的是 Int 類型,都是非 Optional 的。
// map 的閉包接受的是 Int 類型,返回的是 Int 類型,都是非 Optional 的。 let tq: Int? = 1 tq.map { (a: Int) -> Int in a * 2 }
咱們剛剛說,對於 Monad 來講,它和 Functor 的差別實在過小,小到就只有閉包的參數類型不同。數組實現了 flatMap
,它就是一種 Monad,下面咱們就看看 flatMap
在數組中的函數定義,咱們能夠看出,閉包接受的是數組的元素,返回的是一個數組(封裝後的值)。
// 閉包接受的是數組的元素,返回的是一個數組(封裝後的值) let arr = [1, 2, 3] let brr = arr.flatMap { (element:Int) -> [Int] in return [element * 2] }
下面是 flatMap
在 Optional 中的定義,咱們能夠看出,閉包接受的是 Int 類型,返回的是一個 Optional(封裝後的值)。
// 閉包接受的是 Int 類型,返回的是一個 Optional(封裝後的值) let tq: Int? = 1 tq.flatMap { (a: Int) -> Int? in if a % 2 == 0 { return a } else { return nil } }
因此本質上,map
和 flatMap
表明着一類行爲,咱們把這類行爲叫作 Functor 和 Monad。它們的差別僅僅在於閉包函數的參數返回類型不同。因此,咱們纔會把數組和 Optional 這兩個差異很大的類型,都加上兩個實現差異很大的函數,可是都取名叫 map
和 flatMap
。
咱們在第一節燒腦文章中提到過多重 Optional,在使用 map
的時候不仔細,就會觸發多重 Optional 的問題。好比下面這個代碼,變量 b
由於是一個兩層嵌套的 nil,因此 if let
失效了。
let tq: Int? = 1 let b = tq.map { (a: Int) -> Int? in if a % 2 == 0 { return a } else { return nil } } if let _ = b { print("not nil") }
解決辦法是把 map
換成 flatMap
便可。
討論完了,咱們總結一下:
map
和 flatMap
函數。flatMap
有兩個重載的實現,一個實現等價於先 map
再 flatten
,另外一個實現用於去掉結果中的 nil。map
和 flatMap
函數內部的機制。map
和 flatMap
的取名問題,最後得出:一個類型若是支持 map
,則表示它是一個 Functor;一個類型若是支持 flatMap
,則表示它是一個 Monad。map
中使用不當形成的多重 Optional 問題。
轉載自:http://www.infoq.com/cn/articles/swift-brain-gym-map-and-flatmap?utm_campaign=rightbar_v2&utm_source=infoq&utm_medium=articles_link&utm_content=link_text