从实际应用理解Monad

Jason Yu
Jason Yu
Jul 30, 2017 · 7 min read

说起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,还有阮一峰翻译的中文版

知乎上的回答

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade