版权归作者所有,任何形式转载请联系作者。

作者:tison(来自豆瓣)
来源:https://www.douban.com/note/733279598/

Monad 在实际开发中的应用

不同的人会从不一样的角度接触 Monad。大多数网上的教程和介绍都从其严格的定义出发,加上几个玩具示例就当讲解完毕。诚然,不少 FP 的爱好者都是形式逻辑的拥趸或强于数学的,但是我对 Monad 的理解却不是从其定义入门的。相反,我是先频繁接触了其实例,这其中包括所有开发者都熟悉的列表(List),现代开发者应该熟悉的 Option/Maybe/Optional 和进一步的 Try/Either/Result,以及并发程序开发者熟悉的 Promise 等。当某天我忽然看到某一段文字提到说这些实例就是 Monad 的时候,结合我自己的使用经历,突然能够理解其定义的来由和所要解决的问题。或许这就是一个平凡的开发者接收编程手段演进的过程吧,即从实践经验出发,总结规律并对应到定义中来。

我也不是很明白怎么从定义和抽象实例中去讲明白 Monad 是什么,有什么用。所以按照我自己的尤里卡路径,我打算从它的几个经典实例出发,希望能帮助你思考这些抽象和名词背后的一般思想。这里我会提及 Try, Promise 和 List,不会包括函数式拥趸热爱的 IO Monad,因为后者非常违反纯函数式以外的世界的直觉。

Try

第一个要讲的是 Try,这是考虑到并发编程暂时还没有成为必备技能,Promise 并不是人人都会遇到的,而 List 开发者过于熟悉,从另一个角度看可能会有点反直觉。

Try 要解决的问题和传统的 try-catch 控制块是相似的,也就是处理错误和异常。我们来看一下传统的 try-catch 控制块写出来的代码给人的直观感受。

try {       ... // some initializations       ... // some operations that may cause Exception } catch (XxxException e) {     ... // ideally we do recovery     ... // but most of time we log and rethrow     ... // or swallow it } finally {     ... // some cleanups that must be done }

这个结构在不嵌套的时候以及在 try 中只包含少数语句的时候看起来还不错,因为我们还能很清楚地知道我们在做什么。但是这个前提条件隐含着两个问题。其一,由于 try 开启了一个新的作用域的缘故,我们很多时候会写一个很大的 try 块,而不假思索的大 try 块会让我们忘记到底 try 里面的语句哪个会发生什么异常,以至于即使抛出了异常,我们也只知道异常发生了,而不知道是谁由于什么缘故触发的。如果我们细分的拆成若干个小 try 块,那么我们很快会被满屏的缩进和由于新作用域的缘故定义在 try 外而使用在 try 之后的值,以及需要额外做的 null check 干扰得无法阅读实际业务代码。其二,有的时候我们通过嵌套的方式来处理需要具体 catch 和恢复的可能抛出异常的语句,但是这种缩进正如后面要在 Promise 里讲的 callback hell 一样,会快速的让你失去层次的敏感度。实践经验指出只要有两层 try-catch 就能让一个新接手代码的开发者对这块代码晕菜。

那么 Try Monad 是怎么解决这个问题的呢?我们来看一段典型的 Try 代码

val readFromFile = Try { /* IO */ } // possible IOException val parseTheContent = readFromFile.flatMap(parse _) // possible ParseException  val tolerantParseException = parseTheContent.recoverWith {   case _ : ParseException => /* try to fix and retry */ }  tolerantParseException.map(...)/* ... */

这段代码首先通过 Try { ... } 构造 Try Monad 的实例,这对应 Haskell Monad 中的 return 函数,即把一个类型升格为 Monad。我们直接看这个函数做了什么

object Try {   /** Constructs a `Try` using the by-name parameter.  This    * method will ensure any non-fatal exception is caught and a    * `Failure` object is returned.    */   def apply[T](r: => T): Try[T] =     try Success(r) catch {       case NonFatal(e) => Failure(e)     }  }

我们忽略 NonFatal 这个问题,这段代码的意味是执行一个可能抛出异常的操作,如果操作成功,返回其返回值,如果抛出异常,则记录异常。Try 有两个子类

final case class Success[+