Monad 最简介绍

Monad 最简介绍

Haskell 是一门非常独特的编程语言,哪怕在函数语言中也很特立独行。它以彻底的纯函数和强大的类型系统闻名。Monad 就是由 Haskell 第一个引入编程世界的,它可算作目前为止编程中最难理解的概念了。几乎所有费心尽力最终理解了 Monad 的人都会有一种恍然顿悟的感觉,而且还忍不住会写一篇文章,跟大家分享他理解的 Monad 是什么,我也不例外。

1 为什么 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 操作能够和平相处,共同组织出既安全又有用的程序。

2 Monad 的原理

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

我们拿 IO 操作做例子分析,Haskell 中专门有一个类型类(类似 C++ 中的 Concept 提案) “IO” 用来标示某类型是带有附加的外部 IO 动作。

为了充分隔离纯函数与 IO 函数,Haskell 中不能实现 IO Char -> Char 这样一种输入是 IO 类型返回值却是普通类型的函数。否则的话,副作用函数就可以通过这个函数来把 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

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

echo = >>= getChar . putChar

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

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

  • 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 语法糖又进一步简化了 >>= 复合的语法,使其成为很多类似问题的通用解决方案,这里就不展开了。

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

编程语言进步的三个阶梯是抽象,抽象,更高层次的抽象!


转载自 :道可道 | Monad 最简介绍 http://zhuoqiang.me/a/what-is-monad

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值