道可叨

Free Will

Monad 最简介绍

Haskell 是一门特殊的编程语言,哪怕在函数语言中也很特立独行。它以彻底的纯函数和强大的类型系统闻名。Monad 概念就是由 Haskell 第一个引入编程世界的,它也许是编程领域最难理解的概念。几乎所有费心尽力最终理解了 Monad 的人都会有一种恍然顿悟的感觉,这里简单说说我悟到的 Monad。

为什么 Monad

Haskell 为什么会引入 Monad?最大的原因是为了在纯函数语言中引入副作用。

纯函数的优点是安全可靠。函数输出完全取决于输入,不存在任何隐式依赖。它的存在如同数学公式般完美无缺。可有时越是完美的东西就越没有用处,比如纯函数,因为隔绝了外部环境,纯函数连基本的输入输出都做不了。一个简单的 Hello world 就能难倒纯函数。为了引入 IO 操作,各种函数式语言八仙过海各显神通。Monad 就是 Haskell 给出的方案。并且 Monad 并不仅仅是 IO 操作的抽象,它更是多种类似操作之间共性的抽象。所以 Monad 解决的问题并不局限在 IO 上,像 Haskell 中的 Maybe[] 都是 Monad。Haskell 中漂亮的错误处理方式和灵活的列表推导式 (list comprehension) 也都算是 Monad 的贡献。

我自己一直有种误解,这里需要特别说明一下: Monad 不但不是引入 IO 的唯一方法,而且可以说 Monad 并没有把副作用引入纯函数中。纯函数不能有副作用,有副作用的不叫纯函数,哪怕用了神秘难解的 Monad 也不行。那 Monad 到底做了什么呢?

让我们再回想一下: 纯函数安全可靠但无用,是个无趣的好男人; 普通函数能力强但 Bug 多,对程序来说却是必不可少的。如同危险而有魅力的坏男孩。如何同时拥有两者,让它们合作无间各自发挥特点而不互相打架呢?这就是 Monad 的作用了。它将带有副作用的 IO 操作以一种可控的方式引入到 Haskell 中,让纯函数与 IO 操作能够和平相处,共同组织出既安全又有用的程序。

Monad 的原理

函数之间要协作,就必须以各种形式交互连接。因为 Haskell 使用静态强类型系统,函数间的连接受限于入参与返回值类型,这大大增强了程序的安全性,同时也带来一个问题:如何既充分隔离纯函数与副作用函数,又能让两类函数相互复用?

我们拿 IO 操作做例子分析,Haskell 中专门有一个类型类(类型类有点象 C++ 中的 Concept) IO 用来标示某类型附带有 IO 动作。

为了充分隔离纯函数与 IO 函数,Haskell 中不能实现 IO Char -> Char 这样一种输入是 IO 类型返回值却是普通类型的函数。否则副作用函数就能很容易变身为纯函数了

Char -> Char = (Char -> IO Char) . (IO Char -> Char)

事实上一旦参数中有 IO,返回值必有 IO,这就保证了充分隔离。

那如何让纯函数与 IO 函数相互复用呢?这就要靠 IO Monad 中定义的 return>>= 这两个函数了。return (在 Haskell 中不是关键字,只是一般的函数名)的作用是将某个类型为 A 的值 a 以最自然的方式提升(装箱)为类型为 IO A 的值 Char -> IO Char 。后面会看到所谓“最自然”的方式是有严格定义的。有了这个函数后,纯函数就可以通过与 return 复合变成返回值为 IO 带副作用的函数了,当然,实际的 IO 动作是 Haskell 内建的,return 的主要意义在于类型提升。

有了提升可没有下降操作,怎么复合 putChar :: Char -> IO()getChar :: IO Char 呢。 getChar 从 IO 读取一个字符, putChar 把字符写入 IO。但 getChar 返回的是 IO Char 类型,而 putChar 需要的是普通的 Char 类型,两者不匹配怎么办? >>= 函数出马了! >>= 的类型是

IO a -> (a -> IO b) -> IO b

这样 >>= 就可以连接 getCharputChar ,把输入复写到输出中

echo = (getChar >>= putChar)

可以看到 >>= 操作实际上是受限的类型下降(或拆箱)操作,只有当后面的函数返回值也是 IO 类型时才进行下降操作。这样既充分隔离纯函数与副作用函数,又能让函数相互复用。

通过 return>>= 两个平行世界 (范畴) 就有了可控的交流通道。下图直观的反映 Monad 的作用,A 与 IO A 是分属两个不同的世界,A 是纯洁类型,IO A 是带 IO 操作的类型。为了保证整个程序的质量,两个世界的交流只能以图上的方式进行

monad
  • return 是最自然的类型提升函数,将 a 提升为 a'
  • a -> b' 是普通的提升函数
  • >>= 是受控的类型下降,只有当 b -> c' 存在时,才能将 b' 降到 b。这样 a -> b'b -> c' 就能复合成 a -> c'
  • Monad 没有定义 c' -> c,不存在这种不受控的下降方式

这里,IO 类型类可以是任意符合 Monad 定义的类型类。a' 可以是 IO a, Maybe a, 或者 [a]。同时 Haskell 的 do 语法糖又进一步简化了 >>= 复合的语法,使其成为很多类似问题的通用解决方案,这里就不展开了。

Monad 是什么

Monad 之所以难以理解,就在于它的抽象性。这不同于面向对象,鹰是一种鸟这种程度的类比就足以让人理解子类父类继承关系这些概念了。Monad 的抽象是形而上的高度抽象。它本身是抽象代数中范畴学的一个概念,是特殊的算子。要真正消化它首先要理解抽象的对象,类型,范畴,函子这些概念。没有这些概念打底,理解 Monad 可谓是空中楼阁无根之木。如果非要单独理解 Monad,那上图就是一个很好的简化说明。

前面说了,return 是最自然的提升方式,这里“最自然”是有明确意义的。指的是 return>>= 这对函数必需满足下面的公理

  • return>>= 互为逆操作

    (return x) >>= f == f x
    m >>= return == m
    
  • >>= 满足结合律

    (m >>= f) >>= g == m >>= (\x -> (f x >>= g))
    

这些公理保证了 Monad 正常工作。但可惜,它们在 Haskell 中没法用类型系统加以检查,只能靠各个 Monad 的实现者保证,算是潜规则。正如动态语言需要大量测试覆盖代码以保障质量一样,静态类型语言也需要外在的检验来保证这些潜规则没被违反。

Monad 被引入进 Haskell 用来解决 IO 操作可谓神来之笔,正如面象对象概念可以在非面象对象的语言中模拟使用一样,Monad 概念也能在其它中语言中应用 (比如 Java, C++, Python)。但只有在 Haskell 这种拥有强大类型推导系统的强类型语言中,Monad 才能发挥最大功效。Haskell 或没有跻身为主流语言的一天,但相信 Monad 会象 Lambda 概念一样,从函数语言的王谢家飞入寻常主流语言中来。

编程语言进步的方向是抽象,抽象,更高层次的抽象!