对于 Haskell 初学者来说,Monad 和 IO 或许是掌握 Haskell 之路上的第一大难关。本文将会以尽量浅显的方式介绍 Monad 和 IO 背后的原理和设计思想,希望能够给 Haskell 初学者们一些思考与启发。
本文假设您对函数式编程有一定的理解,因为这是讨论 Monad 和 IO 的理论基础。同时,本文会使用到一些 Haskell 基础语法,比如函数类型定义等。
本文需要您对偏函数和柯里化(currying)有所了解。如果您对此不了解,您可以参考网络上的其它文章。为了减轻读者的理解负担,本文仅在必要时采用柯里化的函数定义形式。
前言
我们知道,纯函数式编程中的函数必须是无副作用的。也就是说,这些函数 不能拥有状态,也 不能改变外界的状态(比如使用全局变量、在屏幕上输出信息),并且每一个输入必须 唯一对应一个输出。
但是,现实中的程序几乎都需要进行一些有副作用的操作。比如,一个程序至少应该能够输出一些信息,或者写入一些文件等,否则这个程序将会毫无意义。因此,即使是函数式编程语言,也有必要引入有副作用的操作。
在 Haskell 中,与副作用有关的两个最重要的概念是 Monad 和 IO。本文将由浅入深地介绍这两个概念。
本文部分译自 Noel Winstanley 的文章 What the hell are Monads?。如果您对本文内容有不同见解,欢迎您对本文提出改进意见。
状态的引入
既然函数必须是无状态的,那么我们不妨先设法引入状态,作为讨论 IO 的一个开始。
假设我们用 Haskell 编写了一个数据库系统,并且提供一个 update
函数用于更新数据库内的记录。update
函数的定义如下:
update :: (DB, Int) -> Bool
update
函数向数据库中写入一个 Int
值,并返回一个 Bool
值表示操作是否成功。
但这样的定义是存在问题的,因为 update
函数必然会改变 DB
的状态,而根据函数式编程的基本规则,参数 DB
是不可变的,update
函数无法修改 DB
的状态。
解决方法也很简单,既然 update
会改变 DB
的状态,那么我们不妨让 update
函数除了返回一个 Bool
值外,还返回一个 更新了状态之后的 DB
,如下:
update :: (DB, Int) -> (DB, Bool)
经过这样的修改,我们实际上就已经能够让 DB
对象拥有 内部状态 了。(外部状态 稍微复杂一些,这个问题将会在之后讨论。)但这样的实现方式会引起一个附加问题:它让 不同操作之间的组合 变得复杂了。假如我们还定义了一个 query
函数,如下:
query :: DB -> (DB, Int)
然后,我们的主程序需要做如下的事情:在数据库中查询一个值 x x x,然后把 x + 1 x + 1 x+1 写回数据库中。那么,我们的主程序就需要这样编写:
(x, db1) = query db
(ok, db2) = update (db1, (x + 1))
问题就在于,我们不能直接把 query
的返回值加