揭开 Monad 的神秘面纱

我们知道 Swift 语言支持函数式编程范式,所以函数式编程的一些概念近来比较火。有一些相对于 OOP 来说不太一样的概念,比如 Applicative, Functor 以及今天的主题 Monad. 如果单纯的从字面上来看,很神秘,完全不知道其含义。中文翻译叫做单子,但是翻译过来之后对于这个词的理解并没有起到任何帮助。

我的理解很简单,Functor 是实现了 map 函数的容器,Monad 就是实现了 flatMap 方法的容器,比如在 Swift 里,Optional, CollectionType 等等都可以称为 Monad。

既然有了 map, flatMap 又有什么作用呢?两者有什么联系和区别呢?

map vs flatMap

map 和 flatMap 的共同点都是接受一个 transform 函数,把一个容器转换为另外一个容器。

下面主要从维度这一块来解释两者的区别,我们先来简单的定义一下维度

对于类型 T,如果类型 S 是一个容器,且元素的类型包含 T,那我们就说:
S (维度) = T (维度) + 1

举个🌰, [Int] (维度) = Int (维度) +1, Int? (维度) = Int (维度) + 1.

map 和 flatMap 的区别是,对于 map,容器里的一个元素经过 transform 后只产生一个元素,是 one-to-one 的关系,也就是说经过转换后,纬度是不变的。比如:

1
2
3
4
5
var intArray: [Int] = [1, 2, 3, 4, 5]
var stringArray: [Int] = intArray.map { (value: Int) -> String in
return "\(value)"
}
//stringArray: ["1", "2", "3", "4", "5"]

这个 transform 函数的是 Int -> Int 的,两边的维度是一致的。

对于 flatmap,容器里的一个元素经过 transform 可能转换成 0 个,1 个 或者多个元素,也就是 one-to-any 的关系,既然是 any 的关系,就需要一个容器来存放 any 个元素,所以经过 transform 的返回值通常是一个容器,所以 transform 函数执行之后,相当于维度+1。

1
2
3
4
var oddIntArray: [Int] = intArray.flatMap { (value: Int) -> Int? in
return value % 2 == 1 ? value : nil
}
//oddIntArray: [1, 3, 5]

这里的 transform 是 Int -> Int? 的,我们知道 Int? 是 Int 的包装类型,所以说 transform 相当于对每个元素都包了一层,提升了一个维度.

但是我们看一下上面例子里 stringArray 和 oddIntArray 的类型,都是 [Int],也就是说 flatMap 函数对 transform 函数的返回值做了降维处理。那么 flat 的意思在这里也就知道了,就是把 transform 返回的容器降维攻击 (拍扁),拿出里面的元素。

flatMap 函数为什么要这么做呢?在函数式编程中,通常会对一个值 / 操作进行链式操作,为了保证后面还可以继续方便的进行链式操作,一般需要保持维度不变。其实可以看作一个约定,大家都遵循一定的规则,才都有得玩。

如何确定使用 map or flatMap 的时机?

从上面可以看到 map 对 transform 的返回值没有做特殊的处理,flatMap 对于 transform 的返回值会做降维处理,比如 unwrap optional 值等。

其实可以反推,如果给定的 transform 函数会对调用者容器里的每个元素做升维,那我们需要用 flatMap 对它的结果进行降维,来保证调用 flatMap 前后维度保持一致。如果说 transform 调用前后维度没有变化,使用 map 方法就行了。

Swift 中的 map 和 flatMap 方法

首先看看 Optional 的 map 和 flatMap 方法:

1
2
3
4
5
6
/// If `self == nil`, returns `nil`.  Otherwise, returns `f(self!)`.
@warn_unused_result
public func map<U>(@noescape f: (Wrapped) throws -> U) rethrows -> U?
/// Returns `nil` if `self` is `nil`, `f(self!)` otherwise.
@warn_unused_result
public func flatMap<U>(@noescape f: (Wrapped) throws -> U?) rethrows -> U?

map 的 transform 是 Wrapped -> U 维度不变,flatMap 的 transform 方法是 Wrapped -> U?,维度 + 1。因为 Optional 的特殊性,flatMap 提供了 one-to-zero/one 的关系。

继续看看 CollectionType:

1
2
3
4
5
public func map<T>(@noescape transform: (Self.Generator.Element) throws -> T) rethrows -> [T]

public func flatMap<T>(@noescape transform: (Self.Generator.Element) throws -> T?) rethrows -> [T]

public func flatMap<S : SequenceType>(transform: (Self.Generator.Element) throws -> S) rethrows -> [S.Generator.Element]

有一个 map 函数和两个 flatMap, map 的 transform 函数是 Element -> T 维度不变,两个 flatMap 的 transform 函数分别是 Element -> T? (one-to-zero/one) 和 Element -> S: SequenceType, SequenceType 是个集合,相当于 one-to-any,这两个 transform 维度都升了一级。

特别感谢我的同事 王轲 , 本文的很多思路都得益于和他的讨论。