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阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容