swift開發:map和flatMap使用

一、數組中的 map 和 flatMap

數組中的 map 對數組元素進行某種規則的轉換,例如:

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 的定義有兩個重載的函數,這兩個重載的函數都是接受一個閉包作為參數,返回一個數組。但是差別在于,閉包的定義不一樣:

  • 第一個函數閉包的定義是:(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 方法,將數組中的元素一一取出來,組成一個新數組。
所以,剛剛的代碼其實等價于:

let arr = [[1, 2, 3], [6, 5, 4]]
let crr = Array(arr.map{ $0 }.joined())
// 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)") 
}
三、Optional 中的 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 方法,我們只能這么寫:

let date: NSDate? = NSDate()
let formatter = DateFormatter()
formatter.dateFormat = "YYYY-MM-dd"
var formatted: String? = nil
if let date = date {
    formatted = formatter.string(from: date as Date)
}

而使用 map 函數后,代碼變得更短,更易讀:

let date2: NSDate? = NSDate()
let formatter2 = DateFormatter()
formatter2.dateFormat = "YYYY-MM-dd"
let formatted2 = date2.map(formatter2.string)

當我們的輸入是一個 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 的源碼

數組的 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
}

對于這個代碼,我們可以看出,它做了以下幾件事情:

  • 1.構造一個名為 result的新數組,用于存放結果。
  • 2.遍歷自己的元素,對于每個元素,調用閉包的轉換函數 transform,進行轉換。
  • 3.將轉換的結果,使用 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
}

也用同樣的方式,把該函數的邏輯理一下:

  • 1.構造一個名為 result 的新數組,用于存放結果。(和另一個重載函數完全一樣)
  • 2.遍歷自己的元素,對于每個元素,調用閉包的轉換函數 transform,進行轉換。(和另一個重載函數完全一樣)
  • 3.將轉換的結果,判斷結果是否是 nil,如果不是,使用使用 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"]
五、Optional 的 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 的這兩函數真的是驚人的相似,如果你只看兩段函數的注釋的話,甚至看不出這兩個函數的差別。
這兩函數實現的差別僅僅只有兩處:

  • 1.f函數一個返回 U,另一個返回 U?。
  • 2.一個調用的結果直接返回,另一個會把結果放到 .Some 里面返回。

兩個函數最終都保證了返回結果是 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函數和 Optinal 的 map函數的實現差別巨大?但是為什么都叫 map這個名字?
  • 數組的 flatMap函數和 Optinal 的 flatMap函數的實現差別巨大?但是為什么都叫 flatMap這個名字?
  • 數組的 flatMap有兩個重載的函數,兩個重載的函數差別巨大,但是為什么都叫 flatMap這個名字?

背后的原因可以參考:http://www.mokacoding.com/blog/functor-applicative-monads-in-pictures/

  • 數組和 Optional 的 map函數都叫一樣的名字,是因為它們都是 Functor
  • 數組和 Optinal 的 flatMap函數都叫一樣的名字,是因為它們都是 Monad

Functor
Functor 在 Wikipedia 上的定義非常學術。一個相對比較容易理解的定義:所謂的 Functor,就是可以把一個函數應用于一個「封裝過的值」上,得到一個新的「封裝過的值」。通常情況下,我們會把這個函數叫做 map。
什么叫做「封裝過的值」呢?數組就是對值的一種封裝,Optional 也是對值的一種封裝。如果你愿意,你也可以自己封裝一些值,比如把網絡請求的結果和網絡異常封裝在一起,做成一個 enum(如下所示)。

enum Result<T> {
case Success(T)
case Failure(ErrorType)
}

一個值能否成為「封裝過的值」,取決于這個值的類型所表示的集合,通過 map函數,能否映射到一個新集合中。這個新集合,也要求能夠繼續使用 map函數,再映射到另外一個集合。
用數組和 Optional 類型來檢查這個規則,就會發現是符合的:

  • 數組可以通過 map函數,生成一個新的數組,新的數組可以繼續使用 map函數。
  • Optional 可以通過 map函數,生成一個新的 Optional 變量,新的 Optional 變量可以繼續使用 map函數。

所以,數組 和 Optional 都是 Functor。

Monad
如果你能理解 Functor,那么 Monad 就相對容易一些了。所謂的 Monad,和 Functor 一樣,也是把一個函數應用于一個「封裝過的值」上,得到一個新的「封裝過的值」。不過差別在于:

  • Functor 的函數定義是從「未封裝的值」到「未封裝的值」的
  • Monad 的函數定義是從「未封裝的值」到「封裝后的值」的。

下面我舉例解釋一下:
剛剛我們說,數組 和 Optional 都是 Functor,因為它們支用 map
函數做「封裝過的值」所在集合的變換。那么,你注意到了嗎?map 函數的定義中,輸入的參數和返回的結果,都不是「封裝過的值」,而是「未封裝的值」。什么是「未封裝的值」?

  • 對于數組來說,「未封裝的值」是數組里面一個一個的元素,map 函數的閉包接受的是一個一個的元素,返回的也是一個一個的元素。
  • 對于 Optional 來說,「未封裝的值」是 Optional 解包出來的值,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和 flatMap函數。
  • 數組的 flatMap有兩個重載的實現,一個實現等價于先 map
    再 flatten,另一個實現用于去掉結果中的 nil。
  • 通過閱讀源碼,我們更加深入理解了 map和 flatMap函數內部的機制。
  • 通過討論 map和 flatMap的取名問題,最后得出:一個類型如果支持 map,則表示它是一個 Functor;一個類型如果支持 flatMap,則表示它是一個 Monad

參考:
http://blog.devtang.com/2016/03/05/swift-gym-4-map-and-flatmap/
http://blog.leichunfeng.com/blog/2015/11/08/functor-applicative-and-monad/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內容