谈谈 Swift 中的 map 和 FlatMap 第二篇 - 另一层思维方式



- 作者: SwiftCafe


前面我们说到 map 和 flatMap 在 Swift 数组中的应用。 但其实 map 和 flatMap 不只存在于数组中, 同样也存在于 Optional 中。 这次我们就说说 Optional 中的 map。 让我们用另一个维度来看待它。

上一篇文章中,我们介绍了 map 和 flatMap 在数组中的应用和实现, 这次我们继续延伸上次的注意,看一看 map 在 Optioanl 中的定义。

你还可以看一下上篇文章: 谈谈 Swift 中的 map 和 flatMap

Optional 中的 map

我们查看 Swift 的文档, 还会发现除了数组类型定义了 map 方法, 同样 Optional 也存在这个方法。 我们来看一下 Optional 中关于 map 的定义:

1
func map<U>(@noescape f: (Wrapped) throws -> U) rethrows -> U?

去掉一些无关项之后, 就是这样:

1
func map<U>(f: (Wrapped) -> U) -> U?

这个 map 方法接受这样一个闭包 (Wrapped) -> U 。 它的参数是 Wrapped 类型。 Wrapped 又是什么类型呢, 我们再进一步看 Optional 的定义:

1
2
3
4
public enum Optional<Wrapped> {
case None
case Some(Wrapped)
}

Wrapped 其实是一个泛型, 代表的 Optional 解包后的值类型。 比如我们有这样一个变量 var a:Int?, 它是 Optional 类型,它里面的 Wrapped 类型所指的就是 Int

再回到我们刚才的 map 方法中, 它的闭包接受的其实是已经解包后的值。 我们来看一个实际的例子:

1
2
var num:Int? = 2
var added = num.map { $0 + 3 } // 5

我们在闭包中,将 num 的值加 3, 然后返回给 added 变量。 并且闭包中我们是直接引用的参数变量 $0, 而没有使用 $0! 或者 if let 这样的解包语法。 也就是说解包的过程, map 方法已经替我们做了。

Optional 中的 map 方法, 还有一个额外效应, 就是可以帮我们解包 Optional 变量。 甚至还能帮我们过滤掉 nil 的情况:

1
2
var nilNum:Int?
var addResult = nilNum.map{ $0 + 3 } // nil

这次 nilNum 变量中没有任何的值, map 方法就会过滤掉这个它, 闭包中的代码也不会被执行。 上面的代码相当于我们使用 if let 来解包的语法:

1
2
3
4
5
6
7
8
9
var nilNum:Int?
var addResult:Int?
if let num = nilNum {
addResult = num
}

相比之下, 使用 map 来完成这个操作,会让代码简洁很多。

对 map 方法合理的使用,能让我们的代码看起来更加舒服。

Optional 中 map 的实现原理

那么 map 方法到底是如何实现呢,它是如果将 nil 值过滤掉的呢? 还是来看看它的源码:

1
2
3
4
5
6
7
8
9
10
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
}
}

看,它的实现如此的简单, 它先判断了一下自身的状态, 如果是 some 状态(也就是有值), 那么就将它解包,然后传入闭包。 如果是 none 状态, 那么就直接返回 nil。 闭包根本不会执行。 关于 Optional 的基础知识这里就不多说了。

大家可以看看之前关于 Optional 的这篇文章了解: 浅谈 Swift 中的 Optionals

看完源代码之后, 就非常清楚了。 定义在 Optional 中的 map 方法, 实际上就是讲自身解包后传入闭包, 然后将闭包的返回值再次打包成 Optional 中返回的这么一个过程。

Optional 中的 flatMap

说完 map 之后,再来看看 Optional 中定义的 flatMap 方法。 还是先来看看它的定义:

1
public func flatMap<U>(@noescape f: (Wrapped) throws -> U?) rethrows -> U?

去掉无关紧要的内容之后:

1
public func flatMap<U>(f: (Wrapped) -> U?) -> U?

它的方法签名几乎和 map 相差无几, 唯一不同的就是闭包参数的返回值类型。 大家回忆一下 map 的闭包类型,是不是这样的: (Wrapped) -> U 而 flatMap 的闭包类型是这样的 (Wrapped) -> U?

没错! 就在最后多了一个问号。

Optional 的 flatMap 和 map 的唯一区别就是,它的闭包也可以返回一个 Optional 值, 也就是说,也可以返回 nil。

我们接下来看一下 flatMap 的例子:

1
2
var value: Double? = 10
var intVal = value.flatMap { Int($0) }

这次我们使用了 flatMap, 在它的闭包中,我们将解包后的值用 Int 进行初始化, 因为 Int 构造方法的返回结果是 Optional 类型,所以我么你这里用 flatMap 是没有问题的。

为什么这样设计

看到这里, 大家可能会产生很多疑问了。 为什么多出这个 flatMap 函数? 数组和 Optional 两个看起来完全不同的东西,为何都会有 map 和 flatMap?

这其实涉及另外一个维度的概念, Functors 和 Monads。 明白这个概念之后,你就会发现这其中的关联,以及为什么会有 map 和 flatMap 这两个函数存在了。

  1. Functors

首先来说说什么是 Functors。 我们首先要了解一个概念, 叫做封装值, 我们定义的普通变量, 比如 let intNum = 3, 这样的值不叫封装值。 我们可以直接将他们传递给函数:

1
2
3
4
5
6
func add(num: Int) -> Int{
return num + 1
}
var intNum = 1
add(intNum) // 2

intNum 是普通值, 可以将他们直接传递给 add 函数。 然而如果是这样一个变量呢:

1
2
var optionalNum:Int? = 1
add(optionalNum) //报错

optionalNum 就是一个封装值, 如果我要把它传递给 add 函数, 首先需要对它解包:

1
add(optionalNum!)

封装值的概念咱们就说到这里, 继续回到 Functors, 其实 Functors 就是将封装值直接传递给函数的一种行为。 对应到我们的实际代码上,就是 map 函数了, 再来看一个例子:

1
2
3
4
5
6
func add(num: Int) -> Int{
return num + 1
}
var optionalNum:Int? = 1
optionalNum.map(add) // 2

我们虽然不能直接把 optionalNum 传递给 add 函数, 但我们可以用 map 函数将 add 作为闭包传递进来。 这样的效果实际上就等于将 optionalNum 传递给 add 函数。 也就是 Functors 所定义的将封装的值直接传递给函数这个行为了。

怎么样, 聪明的你思考一下一定会理解这其中的意义了。

如果把这个思维再发散一下, 其实数组也是一种封装值。 数组中的元素你不能直接引用的, 你需要通过下标的方式才能将这个元素从数组中”解包”出来:

1
2
var array = [1,2,3]
add(array[1]) // 3

如果你想把封装值直接应用 add 函数怎么办呢, 依然是用 map 方法:

1
let res = array.map(add) // [2,3,4]

这个操作实际上也是 Functors 的行为。

总的来说 Functors 就是将一个封装值直接传递给函数,并且返回的结果依然是封装值的一种行为。 我们调用定义在 Optional 的 map 函数, 会用闭包将 Optional 中的值进行操作,然后返回值还是一个 Optional。

同样,我们对数组调用 map 函数, 会用闭包将数组中的值进行一些操作, 然后返回值还是一个数组。

从这个维度来思考,是不是就能理解为什么 Optional 和数组,这两个看似没有任何关联的类型,为什么都有 map 和 flatMap 方法了?

Functors 是一个比较理论的概念,它不止存在于 Swift 中,而是一个数学概念, Swift 中的 map 函数只是这个概念的一个实现, 有兴趣的话你可以看看维基百科上对这个概念的解释 https://en.wikipedia.org/wiki/Functor

嗯, 上升到理论层面, 还是比较抽象的哈, 欢迎大家来研究~

  1. Monads

我们再来看看第二个概念 - Monads。 这又是什么鬼~ 简单来说嘛, Functors 对应的是 map 函数, Monads 对应的就是 flatMap 函数啦。

Monads 用一句话来说的话就是, 它将一个封装值传递给一个返回值类型是封装值的函数。

听着听绕嘴~, 我们用一个实际的例子来说明, 我们看一下刚刚定义的 add 函数的返回值类型:

1
2
3
func add(num: Int) -> Int{
return num + 1
}

是 Int 没错吧, 也就是说 add 函数的返回值类型不是封装值。 所以它是不能作为 flatMap 的闭包的。 我们回想一下, 是不是无论是 Optional 或是数组的 flatMap 方法接受的闭包参数都有一个共性,他们的返回值依然是封装置,我们再来看一下定义:

1
2
3
4
5
6
// Optional
public func flatMap<U>(f: (Wrapped) -> U?) -> U?
// 数组
func flatMap<T>(transform: (Self.Generator.Element) -> T?) -> [T]
func flatMap<S : SequenceType>(transform: (Self.Generator.Element) -> S) -> [S.Generator.Element]

Optional 的 flatMap 函数接受的闭包是 (Wrapped) -> U?, 它返回的还是 Optional 类型。 数组的 flatMap 的两个重载接受的闭包分别是 (Self.Generator.Element) -> T? 和 (Self.Generator.Element) -> S, 他们返回的依然还是数组。

map 和 flatMap 的主要区别就是他们所接受闭包的返回类型, map 的闭包返回的是一个普通值, flatMap 的闭包返回的是一个封装值。

怎么样,从这个维度去思考,就更容易理解为什么 Swift 会这样设计它的 API 了吧。

结尾

Swift 中的 map 和 flatMap 两个函数,实际上是对 Functors 和 Monads 这两个数学概念的实现,基于这两个概念,才有了这两个函数。 也就解释了为什么 Optional 和数组这两个看似无关的类型为什么都会有 map 和 flatMap 方法。

如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。




微信公众平台
更多精彩内容,请关注微信公众号


公众号:swift-cafe
邮件订阅
请输入您的邮箱,我们会把最新的内容推送给您: