从实际应用理解Monad

Jason Yu
7 min readJul 30, 2017

--

说起Monad,这个词并不直白,再看它的中文译名“单子”,同样让人云里雾里。其运用的场景,常常跟函数式编程密切相关,这对于iOS开发来说,又是一个距离日常工作比较远的概念。在一开始学习时我们也碰到了一些障碍,再看了几篇文章,听了几次分享,也没有完全理解。

如果从数学或者函数式编程的意义上来解释,会显得非常枯燥——如果我们不知道它对于日常使用有什么帮助,而它本身又比较难懂,又为什么要去了解呢?

我们不妨试试从应用的角度切入,看看Monad究竟能做些什么。希望这篇文章能带给你一些启发。

对于iOS开发来说,OC的年代并没有那么明显的函数式的概念。有一次看到Stackoverflow上有人问OC的链式调用应该怎么写,有人吐槽答:

如果没办法把多个函数优雅地拼接在一起,那写函数式的痛苦可想而知。幸运的是Swift带来了很大的改变。

一个实际的例子

即刻从一开始就是一个纯Swift项目,并且在15年下半年用上了RxSwift,迄今已有两年多的时间,积累了一定的实践经验。

举例来说,发送一个网络请求,在之前不使用Rx时可能这样写:

用了Rx以后:

大括号/条件判断/嵌套缩进全部消失了,简洁的代码带来了可读性的大幅提升(将线程切换再封装可以得到更短的代码),相信用过的人再也不想回到前面那种写法了,优雅、清爽、易维护。

在前一种写法中我们将请求的返回值声明为json,然后通过调用parse方法转换为相应的业务类型model,最后把model传到callback中去。但是在第二种中,我们并没有声明任何的形参,而是把一系列函数组装在一起,让数据从一个一个函数中流过,避免了中间状态的产生。这也是函数式编程的优点之一。

将不同函数组装成链式调用

如果把网络返回的处理流程(类似的还有图像处理等等)比作一个流水线,那每一个部分(函数)都遵守一个约定:接受同一数量(通常是1个)、同一类型的参数。由于参数数量和类型的统一,同时不依赖其他状态,我们可以方便地交换流程中每一步的顺序,或者增加/删除其中一部分,同时不影响到其他的步骤。

那网络返回发生错误怎么办呢?因为成功或失败对应了不同的处理流程,之前我们至少需要两个参数才能完成。将两个或者多个参数变为一个,需要一层额外的抽象。

有趣的是,Monad的本意就是“接受一个参数的函数”,与之对应的还有dyad(接受两个参数的函数)。

Monad第一个条件:Box<T>

得益于Rx的封装,所有网络调用的返回类型都是一个的Observable<T>的实例(具体用法可以参见Rx的文档)。由于Rx的流式处理逻辑中本身就提供了一套错误处理的方法,因此在定义API的代码中我们可以将注意力集中在返回的数据上。

一般来说,对于一个Monad类型,我们需要封装一个Box<T>,将流程中关心的信息放在里面(用的比较多的是正确/错误两种不同的情况)。

Monad第二个条件:flatMap方法(也叫bind/lift)

在对Box<T>链式调用每一步中,需要做以下几件事:

  1. 对Box<T>拆包,拿到我们真正关心的数据T
  2. 处理数据
  3. 仍然封包成Box类型(可能变换了数据,也可能封装类型发生改变,如Box<R>)

可以看到入参与出参同为Box类型,因此只要所有方法都遵守这个规则,它们就可以被拼接起来,如同接口统一的乐高积木一样实现各种各样的可能。

这一整套拆包/封包的逻辑,称作flatMap方法,它构成了Monad的第二个要素

一般flatMap方法的参数类型(在面向对象语言中,flatMap方法的第一个参数就是self,因此第二个参数是唯一参数):

func flatMap<T,U>(box: Box<T>, f: T -> Box<U>) -> Box<U>

由于flatMap方法参数中要求Box<T>作为参数(也就是需要满足刚才提到的Monad第一个条件),因此Chris Eldhof也说

日常使用中的Monad类型

到这里你可能发现,其实我们日常工作中已经用到了不少具有flatMap方法的类型,举几个例子:

  1. Optional<T>,作为Swift的一大feature,大家都非常熟悉了。形如a?.b?.c?.d的语法(Optional chaining),如果当中某一步的值为nil,那么整个表达式的结果也为nil。其实“?.”这个语法就是Optional.flatMap的语法糖,在每一步中先解包,检查值是否存在,如果存在就进行下一步(先封装,因此即使d不是optional,整个表达式最终值仍然是个optional),没有就返回nil,中止下一步调用(跟网络请求很像)。
  2. Array<T>,同时满足了Box<T>和flatMap两个条件,因此它是一个经典的Monad
  3. Observable<T>,就是上面我们所提到的例子。如果你是Rx的熟手,应该很熟悉了。

还有很多类似于Alamofire的Result<T>和PromiseKit的Promise<T>类型,虽然链式调用的方法名不一定叫flatMap,但是理念是非常接近的,同样也能完成链式调用和错误处理。

Haskell中的Monad

作为一个函数式编程语言,在Haskell中对于Monad有着严格的定义,称为Monad laws:

其中return表示构建一个Box对象。第一第二条称之为left/right identity(封装类型并不改变它原来的值),第三条为结合律(链式调用中的任意连续步都可以合并为一步而结果不变)。

事实上,Monad还有更抽象的范畴学定义:自函子范畴上的一个幺半群。尽管我也不太理解,但对于我们工程师来说,并不影响我们使用它,并享受它给我们带来的好处。

总结

  1. 对于日常开发来说,绝大多数逻辑都可以归纳为一个流程:用户操作->数据数处理>API请求->数据处理->显示,每一步都由上一步的结果驱动,而这恰巧是非常适合使用链式调用的(同样也是Rx发扬光大的原因)。
  2. 因为需要链式调用,为了让参数统一,于是将数据和状态封装起来(Box<T>),并规定每一部分函数需要遵守的拆包/封包的约定(flatMap),也就构成了Monad。
  3. 将多个参数合并为一个,经常需要用到Currying的方法。此外,Swift还支持很多其他函数式的技巧,在Functional Swift一书中有提到。
  4. Monad看似高深,其实和我们日常工作息息相关。我们不仅应该从定义和数学角度来理解,更应该从实际使用角度来分析它对我们的价值。

推荐阅读

Wikipedia上的Monad

Runes,实现了一些函数式编程的操作符

Stackoverflow上的回答

图示Functor, Applicative, Monads,还有阮一峰翻译的中文版

知乎上的回答

--

--