书接上文,让我们从 Writer 继续深入下去。
Haskell 中的 Writer
同样的事,在 haskell 中更容易一些。我们先定义一个 Writer 类型:1
type Writer a = (a, String)
这里的 Writer 是一个类型别名,a 是一个参数变量,Writer a 指向一个 a 类型变量和字符串组成的二元组。
我们在上一节中提到的态射实际上是这样的一个函数:1
a -> Writer b
声明一个符号用来描述组合1
(>=>) :: (a -> Writer b) -> (b -> Writer c) -> (a -> Writer c)
上面这个“>=>”是一个中缀运算符,它接受两个参数:第一个的类型是(a -> Writer b),第二个的类型是(b -> Writer c),返回结果的类型是(a -> Writer c)。
我们现在来实现这个函数:1
2
3
4m1 >=> m2 = \x ->
let (y, s1) = m1 x
(z, s2) = m2 y
in (z, s1 ++ s2)
m1、m2 是“>=>”的两个参数,返回结果是一个参数为 x 的匿名函数。
我们接下来实现 Writer 的恒等态射 return:1
2return :: a -> Writer a
return x = (x, "")
现在 haskell 版本的 Writer 实现完了,看起来是不是要简洁很多?那我们可以试试 haskell 版本的 process 了。先来看一下 toUpper 和 toWords 函数:1
2
3
4
5upCase :: String -> Writer String
upCase s = (map toUpper s, "upCase ")
toWords :: String -> Writer [String]
toWords s = (words s, "toWords ")
因为 haskell 里面已经有针对字符的 toUpper 函数,所以这里我们换了个函数名 upCase。
最后,process 登场:1
2process :: String -> Writer [String]
process = upCase >=> toWords
Kleisli 范畴
看到现在,需要补充一下理论上的内容了。前面讲的 Writer 实际上是范畴论中的一种范畴,叫做 Kleisli 范畴。这是一种基于 monad 的范畴,先不用管 monad 是什么,我们来看看 kleisli 范畴能干什么。一个 kleisli 范畴的对象是编程语言中的一个类型,从类型 A 到类型 B 的态射是从 A 到从 B 派生出来的一个类型的函数(这里的派生实质上是范畴论中的自函子)。每个 kleisli 范畴都定义了对应的组合函数和恒等态射函数(compose 和 identity)。
在这一章用来作为范畴基础的叫 writer monad,它用来在 haskell 记录函数的执行情况。它向我们展示了如何将副作用嵌入到纯函数语言中:我们可以在函数的组合中做除了传递参数以外更多的事情。这种方式比传统命令式语言的副作用实现方式多了一个优势:可以给出语言的指称语义。
最后,给一个 Writer 的完整实现(较之标准实现还是简化了一点):1
2
3
4
5newtype Writer w a = Writer { runWriter :: (a,w) }
instance (Monoid w) => Monad (Writer w) where
return a = Writer (a,mempty)
(Writer (a,w)) >>= f = let (a',w') = runWriter $ f a in Writer (a',w `mappend` w')
参考资料