索引
Swift 烧脑系列文章列表:
- Swift 烧脑体操(一) - Optional 的嵌套
- Swift 烧脑体操(二) - 函数的参数
- Swift 烧脑体操(三) - 高阶函数
- Swift 烧脑体操(四) - map 和 flatMap
- Swift 烧脑体操(五)- Monad
- Swift 烧脑体操(六)- 类型推断
前言
Swift 其实比 Objective-C 复杂很多,相对于出生于上世纪 80 年代的 Objective-C 来说,Swift 融入了大量新特性。这也使得我们学习掌握这门语言变得相对来说更加困难。不过一切都是值得的,Swift 相比 Objective-C,写出来的程序更安全、更简洁,最终能够提高我们的工作效率和质量。
Swift 相关的学习资料已经很多,我想从另外一个角度来介绍它的一些特性,我把这个角度叫做「烧脑体操」。什么意思呢?就是我们专门挑一些比较费脑子的语言细节来学习。通过「烧脑」地思考,来达到对 Swift 语言的更加深入的理解。
这是本体操的第四节,练习前请做好准备运动,保持头脑清醒。
我之前一直以为我是懂 map
和 flatMap
的。但是直到我看到别人说:「一个实现了 flatMap
方法的类型其实就是 monad。」我又发现这个熟悉的东西变得陌生起来,本节烧脑体操打算更细致一些介绍 map
和 flatMap
,为了下一节介绍 monad 做铺垫。
准备运动:基础知识
数组中的 map
和 flatMap
数组中的 map
对数组元素进行某种规则的转换,例如:
let arr = [1, 2, 4] |
而 flatMap
和 map
的差别在哪里呢?我们可以对比一下它们的定义。为了方便阅读,我在删掉了定义中的 @noescape
、throws
和 rethrows
关键字,如果你对这些关键字有疑问,可以查阅上一期的烧脑文章:
extension SequenceType { |
我们从中可以发现,map
的定义只有一个,而 flatMap
的定义有两个重载的函数,这两个重载的函数都是接受一个闭包作为参数,返回一个数组。但是差别在于,闭包的定义不一样。
第一个函数闭包的定义是:(Self.Generator.Element) -> S
,并且这里 S 被定义成:S : SequenceType
。所以它是接受数组元素,然后输出一个 SequenceType
类型的元素的闭包。有趣的是, flatMap
最终执行的结果并不是 SequenceType
的数组,而是 SequenceType
内部元素另外组成的数组,即:[S.Generator.Element]
。
是不是有点晕?看看示例代码就比较清楚了:
let arr = [[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]] |
讲完了 flatMap
的第一种重载的函数,我们再来看第二种重载。
在第二种重载中,闭包的定义变成了:(Self.Generator.Element) -> T?
,返回值 T 不再像第一种重载中那样要求是数组了,而变成了一个 Optional 的任意类型。而 flatMap
最终输出的数组结果,其实不是这个 T?
类型,而是这个 T?
类型解包之后,不为 .None
的元数数组:[T]
。
我们还是直接看代码吧。
let arr: [Int?] = [1, 2, nil, 4, nil, 5] |
在这个例子中,flatMap
将数组中的 nil 都丢弃掉了,只保留了非空的值。
在实际业务中,这样的例子还挺常见,比如你想构造一组图片,于是你使用 UIImage 的构造函数,但是这个函数可能会失败(比如图像的名字不存在时),所以返回的是一个 Optional 的 UIImage 对象。使用 flatMap
方法可以方便地将这些对象中为 .None 的都去除掉。如下所示:
let images = (1...6).flatMap { |
Optional 中的 map
和 flatMap
其实 map
和 flatMap
不止存在于数组中,在 Optional 中也存在。我们先看看定义吧:
public enum Optional<Wrapped> : _Reflectable, NilLiteralConvertible { |
所以,对于一个 Optional 的变量来说,map
方法允许它再次修改自己的值,并且不必关心自己是否为 .None
。例如:
let a1: Int? = 3 |
再举一个例子,比如我们想把一个字符串转成 NSDate 实例,如果不用 map
方法,我们只能这么写:
let date: NSDate? = NSDate() |
而使用 map
函数后,代码变得更短,更易读:
let date: NSDate? = NSDate() |
看出来特点了吗?当我们的输入是一个 Optional,同时我们需要在逻辑中处理这个 Optional 是否为 nil,那么就适合用 map
来替代原来的写法,使得代码更加简短。
那什么时候使用 Optional 的 flatMap
方法呢?答案是:当我们的闭包参数有可能返回 nil 的时候。
比如,我们希望将一个字符串转换成 Int,但是转换可能失败,这个时候我们就可以用 flatMap
方法,如下所示:
let s: String? = "abc" |
我在这里还发现了更多的使用 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) |
数组的 flatMap
的源码(重载函数一)
刚刚也说到,数组的 flatMap
有两个重载的函数。我们先看第一个的函数实现。源码地址是:https://github.com/apple/swift/blob/master/stdlib/public/core/SequenceAlgorithms.swift.gyb。
|
对于这个代码,我们可以看出,它做了以下几件事情:
- 构造一个名为
result
的新数组,用于存放结果。 - 遍历自己的元素,对于每个元素,调用闭包的转换函数
transform
,进行转换。 - 将转换的结果,使用
appendContentsOf
方法,将结果放入result
数组中。
而这个 appendContentsOf
方法,即是把数组中的元素取出来,放入新数组。以下是一个简单示例:
var arr = [1, 3, 2] |
所以这种 flatMap
必须要求 transform
函数返回的是一个 SequenceType
类型,因为 appendContentsOf
方法需要的是一个 SequenceType
类型的参数。
数组的 flatMap
的源码(重载函数二)
当我们的闭包参数返回的类型不是 SequenceType
时,就会匹配上第二个重载的 flatMap
函数。以下是函数的源码。
public func flatMap<T>( |
我们也用同样的方式,把该函数的逻辑理一下:
- 构造一个名为
result
的新数组,用于存放结果。(和另一个重载函数完全一样) - 遍历自己的元素,对于每个元素,调用闭包的转换函数
transform
,进行转换。(和另一个重载函数完全一样) - 将转换的结果,判断结果是否是 nil,如果不是,使用使用
append
方法,将结果放入result
数组中。(唯一差别的地方)
所以,该 flatMap
函数可以过滤闭包执行结果为 nil 的情况,仅收集那些转换后非空的结果。
对于这种重载的 flatMap
函数,它和 map
函数的逻辑非常相似,仅仅多做了一个判断是否为 nil 的逻辑。
所以,面试题来了:「什么情况下数组的 map
可以和 flatMap
等价替换?」
答案是:当 map
的闭包函数返回的结果不是 SequenceType
的时候。因为这样的话,flatMap
就会调到我们当前讨论的这种重载形式。而这种重载形式和 map
的差异就仅仅在于要不要判断结果为 nil。
下面是一个示例代码,可以看出:brr
和 crr
虽然分别使用 map
和 flatMap
生成,但是结果完全一样:
let arr = [1, 2, 4] |
Optional 的 map
和 flatMap
源码
看完数组的实现,我们再来看看 Optional 中的相关实现。源码地址是:https://github.com/apple/swift/blob/master/stdlib/public/core/Optional.swift,摘录如下:
/// If `self == nil`, returns `nil`. |
Optional 的这两函数真的是惊人的相似,如果你只看两段函数的注释的话,甚至看不出这两个函数的差别。
这两函数实现的差别仅仅只有两处:
f
函数一个返回U
,另一个返回U?
。- 一个调用的结果直接返回,另一个会把结果放到 .Some 里面返回。
两个函数最终都保证了返回结果是 Optional 的。只是将结果转换成 Optional 的位置不一样。
这就像我老婆给我说:「我喜欢这个东西,你送给我吗?不送的话我就直接刷你卡买了!」。。。买东西的结果本质上是一样的,谁付钱本质上也是一样的,差别只是谁动手而已。
既然 Optional 的 map
和 flatMap
本质上是一样的,为什么要搞两种形式呢?这其实是为了调用者更方便而设计的。调用者提供的闭包函数,既可以返回 Optional 的结果,也可以返回非 Optional 的结果。对于后者,使用 map
方法,即可以将结果继续转换成 Optional 的。结果是 Optional 的意味着我们可以继续链式调用,也更方便我们处理错误。
我们来看一段略烧脑的代码,它使用了 Optional 的 flatMap
方法:
var arr = [1, 2, 4] |
enum Result
case Success(T)
case Failure(ErrorType)
}
|
// map 的闭包接受的是 Int 类型,返回的是 String 类型,都是一个一个的元素类型,而不是数组。
let arr = [1, 2, 4]
let brr = arr.map {
(element: Int) -> String in
“No.” + String(element)
}
|
// map 的闭包接受的是 Int 类型,返回的是 Int 类型,都是非 Optional 的。
let tq: Int? = 1
tq.map { (a: Int) -> Int in
a * 2
}
|
// 闭包接受的是数组的元素,返回的是一个数组(封装后的值)
let arr = [1, 2, 3]
let brr = arr.flatMap {
(element:Int) -> [Int] in
return [element * 2]
}
|
所以本质上,map
和 flatMap
代表着一类行为,我们把这类行为叫做 Functor 和 Monad。它们的差异仅仅在于闭包函数的参数返回类型不一样。所以,我们才会把数组和 Optional 这两个差别很大的类型,都加上两个实现差别很大的函数,但是都取名叫 map
和 flatMap
。
多重 Optional
我们在第一节烧脑文章中提到过多重 Optional,在使用 map
的时候不仔细,就会触发多重 Optional 的问题。比如下面这个代码,变量 b
因为是一个两层嵌套的 nil,所以 if let
失效了。
let tq: Int? = 1 |
解决办法是把 map
换成 flatMap
即可。
总结
讨论完了,我们总结一下: