Haskell Monad (上)
函数式编程中的单子(Monad)
说到pure functional programming,实在是绕不过去Monad的。
pure function的特点
往往用函数式编程的思路写了几个程序之后就会遇到一个问题:针对具有状态更新的程序,似乎总是不太好处理。由于语言的纯粹性,相同的输入总是返回一样的值,所以如果在计算过程中需要更新一个状态,就要去显式地把状态作为参数传递给函数,并显式地构造递归去实现状态更新。这经常是一个繁琐的过程。
譬如(以一个求值器为例):
1、错误处理:如果要在模块中加入错误处理,那么需要在每一次的递归中检查状态并进行处理。(命令式语言中通过异常实现)
2、计数:同样要修改每次的递归。(imperative language中通过全局变量就能搞定。)
3、trace:需要插入调试信息的时候也是需要显式的在递归中插入打印信息。(imperative language中打印就能解决。)
而monad正是解决上述问题的途径。
evaluator的Monad实现
1、evaluator定义
假设term定义如下
data Term = Con Int | Div Term Term
一个Term要么是整数,要么是两个Term的商。
一个简单的evaluator定义如下:
eval :: Term -> Int
eval (Con a) = a
eval (Div t u) = eval t / eval
输入如下两个Term:
answer, error :: Term
answer = (Div (Div (Con 24) (Con 3)) (Con 2))
error = (Div (Con 1) (Con 0))
则eval结果:answer = 4, error = undefined
2、考虑错误处理的eval过程
通过构造新的返回值来支持Exception
data M a = Raise Exception | Return a
type Exception = String
eval :: Term -> M Int
eval (Con a) = Return a
eval (Div t u) = case eval t of
Raise e -> Raise e
Return a ->
case eval u of
Raise e -> Raise e
Return b ->
if b == 0
then Raise "divide by zero"
else Return (a/b) 式 2-1
3、添加状态处理
假设我们现在要记录除法的次数,在命令式语言(譬如C)中通过定义一个初值为0的全局变量,每次除法递增1就可以了。
在纯函数式环境中,定义如下类型
type M a = State -> (a, State)
type State = Int
一个函数类型,输入初始状态,返回最终状态和计算值得组合对。
eval实现如下
eval :: Term -> M Int
eval (Con a) st = (a, st)
eval (Div t u) st = let (a, y) = eval t st in
let (b, z) = eval u y in
(a/b, z+1) 式 2-2
每次做除法的时候都要传入状态st。
3、中间输出调试
类似于上面的思路,定义
type M a = (Output, a)
type Output = String
eval :: Term -> M Int
eval (Con a) = (line (Con a) a a)
eval (Div t u) = let (x,a) = eval t in
let (y,b) = eval u in
(x ++ y ++ line (Div t u) (a/b), a/b) 式 2-3
line :: Term -> Int -> Output
line t a = "eval(" ++ show t ++ ") <==" ++ show a
表达式(x ++ y ++ line (Div t u) (a/b), a/b)
告诉我们,先对x求值,再对y求值。
4、单子化的求值器
从上面的例子可以看出输出类型的特点是具有M a这样的形式。事实上,我们把eval的类型从a -> b
改写成了a -> M b
。
这就是monadic function的形式了。诸如状态、输出、异常等作用都通过M来实现。
现在思考一个问题:Monad需要哪些操作?
从evaluator的例子看出首先要有一个生成Monad的函数unit
unit :: a -> M a
其次从递归过程中看出需要一个函数a -> M b,来对M a进行操作
(*) :: M a -> (a -> M b) -> M b
定义Monad:一个三元组(M, unit, *),包括类型构造子M、两个多态化的操作。通过Monad实现的表达式通常有这样的形式:
是一个lambda表达式,上式理解为执行操作m,把结果绑定到a,然后执行操作n。从类型推导的角度来理解这个表达式是这样的:m的类型是M a,变量a类型为a,表达式n类型为M b,lambda表达式类型为a -> M b,所以整个表达式的类型为M b。
用monad的思路重写eval:
eval :: Term -> M Int
eval (Con a) = unit a
eval (Div t u) = eval t * λa.eval u * λb.unit(a/b) 式 2-4
上式等价于
eval (Div t u) =((eval t) * (λa.((eval u) * (λb.(unit(a/b)))))) 式 2-5
通过monad,我们就能避免每次(譬如带state/output/exception)都要重写求值器。下面一一道来。
5、回到basic evaluator
type M a = a
unit :: a -> I a
unit a = a
(*) :: M a -> (a -> M b) -> M b
a * k = k a
这样的monad称为Identity monad,用该monad重写式2-2得到了和simple evaluator等价的求值器。
6、monad实现异常处理(exception monad)
重新定义Monad如下:
data M a = Raise Exception | Return a
type Exception = String
unit :: a -> M a
unit a = Return a
(*) :: M a -> (a -> M b) -> M b
m*k = case m of
Raise e -> Raise e
Return a -> k a
raise :: Exception -> M a
raise e = Raise e
用这个monad替换2-5式中的Monad定义,然后把unit(a/b)改为
if b == 0
then raise "divide by zero"
else unit(a/b)
即可。这样展开的monadic function和式2-1是等价的。
7、state monad
type M a = State -> (a, State)
type State = Int
unit :: a -> M a
unit a :: λx. (a, x)
(*) :: M a -> (a -> M b) -> M b
m * k = λx. let (a, y) = m x in
let (b, z) = k a y in
(b, z)
tick :: M ()
tick = λx. ((), x+1)
unit a返回(a, x),即结束状态等于初始状态。
通过这个monad来定义2-2式,并且使得unit(a/b)为
tick * λ().unit(a/b)
8、output monad
type M a = (Output, a)
type Output = String
unit :: a -> M a
unit a = ("", a)
(*) :: M a -> (a -> M b) -> M b
m * k = let (x, a) = m in
let (y, b) = k a in
(x ++ y, b)
out :: Output -> M ()
out x = (x, ())
output monad中的unit返回空字符串和值a构成的对,调用(m*k)先从提取输出x和a, 然后从k a提取y和b,最终返回x和y的拼接字符串和值b构成的对。out x返回输出字符串x和空值构成的对。
Monad法则
通过Monad的定义我们可以得出Monad类型类的以下三条法则。
1、左单元操作
unit a * λb.n = n a
2、右单元操作
m * λa. unit a = m
3、结合律
m * (λa.n * λb.o) = (m * λa.n) * λb.o
从上式可以看出结合律并不是所有情况都能满足的,假如a作为一个*变量出现在表达式o中,那么这个推导显然是不成立的。