我以前一直觉得我是懂 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