Swift 烧脑体操(四) - map 和 flatMap
2016-03-07 13:20
639 查看
前言
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] // 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] |
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 }.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)") } |
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? } |
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 |
map方法,我们只能这么写:
var date: NSDate? = ... var formatted = date == nil ? nil : NSDateFormatter().stringFromDate(date!) |
map函数后,代码变得更短,更易读:
var date: NSDate? = ... var formatted = date.map(NSDateFormatter().stringFromDate) |
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,进行转换。(和另一个重载函数完全一样)
将转换的结果,判断结果是否是 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"] |
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 } } |
这两函数实现的差别仅仅只有两处:
f函数一个返回
U,另一个返回
U?。
一个调用的结果直接返回,另一个会把结果放到 .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
和 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/。
数组和 Optional 的
map函数都叫一样的名字,是因为它们都是 Functor。
数组和 Optinal 的
flatMap函数都叫一样的名字,是因为它们都是 Monad)。
好吧,我猜你心里开始骂娘了:「为了解释一个问题,引入了两个新问题:谁知道什么是 Functor 和 Monad !」
不要着急,我们先说严谨的结论有助于更好地总结和归纳,我下面试着解释一下 Functor 和 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) } |
map的闭包接受的是 Int 类型,返回的是 Int 类型,都是非 Optional 的。
// map 的闭包接受的是 Int 类型,返回的是 Int 类型,都是非 Optional 的。 let tq: Int? = 1 tq.map { (a: Int) -> Int in a * 2 } |
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
我们在第一节烧脑文章中提到过多重 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即可。
总结
讨论完了,我们总结一下:数组和 Optional 都能支持
map和
flatMap函数。
数组的
flatMap有两个重载的实现,一个实现等价于先
map再
flatten,另一个实现用于去掉结果中的 nil。
通过阅读源码,我们更加深入理解了
map和
flatMap函数内部的机制。
我们讨论了
map和
flatMap的取名问题,最后得出:一个类型如果支持
map,则表示它是一个 Functor;一个类型如果支持
flatMap,则表示它是一个 Monad。
我们讨论了
map中使用不当造成的多重 Optional 问题。
相关文章推荐
- Swift 烧脑体操(三) - 高阶函数
- swift版本拼图游戏项目源码
- swift版本拼图游戏项目源码
- Swift 烧脑体操(二) - 函数的参数
- Swift 烧脑体操(一) - Optional 的嵌套
- 《从零开始学Swift》学习笔记(Day60)——Core Foundation框架
- 《从零开始学Swift》学习笔记(Day 59)——代码排版
- 《从零开始学Swift》学习笔记(Day58)—— Swift编码规范之变量或常量声明规范
- 《从零开始学Swift》学习笔记(Day 57)——Swift编码规范之注释规范:
- swift
- 《从零开始学Swift》学习笔记(Day 56)—— Swift编码规范之命名规范
- Swift - 按钮(UIButton)的用法
- 《从零开始学Swift》学习笔记(Day 55)——使用try?和try!区别
- 《从零开始学Swift》学习笔记(Day54)——抛出错误
- 《从零开始学Swift》学习笔记(Day 53)——do-try-catch错误处理模式
- 《从零开始学Swift》学习笔记(Day 52)——Cocoa错误处理模式
- 《从零开始学Swift》学习笔记(Day 51)——扩展构造函数
- Swift笔记 使用UIScrollView来居中显示图片,可缩放和跟踪触摸点
- swift 指纹识别
- swift版本拼图游戏项目源码