这篇文章是 Aditya Bhargava 所著 《Three Useful Monads》 的中文译文,已联系原作者取得授权。
This Article is the Chinese translation for Three Useful Monads (Written by Aditya Bhargava).
- 英文原文写于 2013 年 6 月 10 日。
引文
在阅读本文之前,你应当了解
Monad的基本概念,否则请先阅读 《图解 Functor, Applicative 和 Monad》。
下图是函数 half:
我们可以将其连续应用多次:1
2half . half $ 8
=> 2
结果与预期一致。现在你决定记录这个函数的执行过程:
1
half x = (x `div` 2, "I just halved " ++ (show x) ++ "!")
看起来不错。如果我们想将其连续应用多次,又该怎么书写呢?我们无法直接使用 half . half $ 8,因为应用一次 half 的返回值已经变成了元组,我们无法对元组继续应用 half。下图展示了我们实际期望的功能:
显然这个功能不会自己产生,我们必须自己实现:1
2
3finalValue = (val2, log1 ++ log2)
where (val1, log1) = half 8
(val2, log2) = half val1
但是如果需要记录更多的函数呢?这里存在一个模式:我们希望将每个返回 (Value, Log) 的函数 “串” 到一起。这其实是一种副作用,而 Monad 刚好擅长处理这种副作用!
Writer Monad

Writer Monad 非常酷炫。“老铁,让我来处理这波历史记录”,Writer 这么说,“我会帮助你的代码恢复整洁,我还能帮你上天!”(原著这里为 “启动齐柏林飞艇”)。每个 Writer 都包含一个历史记录并回传计算结果。
1
data Writer w a = Writer { runWriter :: (a, w) }
Writer 允许我们这么写代码:1
half 8 >>= half
或者你可以用 <=<,它实现了 Monad 版本的函数复合:1
half <=< half $ 8
非常接近 half . half $ 8 的写法!一颗赛艇!
我们使用 tell 来写入历史记录,用 return 将一个普通的值放入 Writer 的返回值。这是 Writer 版本的 half 函数:1
2
3
4half :: Int -> Writer String Int
half x = do
tell ("I just halved " ++ (show x) ++ "!")
return (x `div` 2)
新的 half 会回传一个 Writer:
runWriter 能帮助我们取出 Writer 封装的元组。
1
2runWriter $ half 8
=> (4, "I just halved 8!")
然而,真正牛逼的地方在于,我们现在可以用 >>= 把 half 串起来了:1
2runWriter $ half 8 >>= half
=> (2, "I just halved 8!I just halved 4!")
下图说明了上面这行代码的原理:
我们不需要写任何繁杂的代码,因为 >>= 知道如何将两个 Writer 合并(做 Monad 最重要的是整整齐齐了)!下面是 >>= 针对 Writer 的完整定义:
其实这就是我们之前写过的样本代码,不过现在 >>= 帮助我们简化了。别忘了我们还有 return,它将一个值放入 Monad 中,对于 Writer 而言其作用如下图:
1
return val = Writer (val, "")
(注意:这些定义 可视作 正确的。实际的 Writer Monad 允许将任何 Monoid 类型作为 “历史记录”,而不仅限于字符串。这里我用字符串简化以帮助你理解。)
感谢 Writer Monad !
Reader Monad
假如你想将一些配置传递给许多函数,不妨试试 Reader Monad!
Reader Monad 允许你将一个值传递给所有幕后的函数。举个例子:1
2
3
4greeter :: Reader String String
greeter = do
name <- ask
return ("hello, " ++ name ++ "!")
greeter 回传一个 Reader Monad:
下面是 Reader 的定义:1
data Reader r a = Reader { runReader :: r -> a }
Reader 的唯一字段是一个函数,runReader 可以取出这个函数:
现在你可以给这个函数一些输入,它们将会被 greeter 应用:
1
2runReader greeter $ "adit"
=> "hello, adit!"
每当你使用 >>= 都会得到一个 Reader,当你向该 Reader 传入一个状态时,这个状态会被传递给 monad 中的每个函数。
1
m >>= k = Reader $ \r -> runReader (k (runReader m r)) r
Reader 有些复杂,不过复杂的才是最吼的。return 将一个值放入 Reader :
1
return a = Reader $ \_ -> a
ask 将传入的状态回传:1
ask = Reader $ \x -> x
想了解更多关于 Reader 的内容吗?你可以在 这里 看到一个更长的例子(需翻墙)。
State Monad
State Monad 是 Reader Monad 最好的朋友:
她看起来和 Reader Monad 非常像,只不过它既可读又可写。这是 State 的定义:1
State s a = State { runState :: s -> (a, s) }

你可以使用 get 获取状态,也可用 put 改变状态。举个例子:1
2
3
4
5
6
7
8greeter :: State String String
greeter = do
name <- get
put "tintin"
return ("hello, " ++ name ++ "!")
runState greeter $ "adit"
=> ("hello, adit!", "tintin")
没毛病!Reader 就像在说 “你无法改变我”,而 State 则对改变持兹瓷态度。State 和 Reader 的定义看起来非常相似:return:
1
return a = State $ \s -> (a, s)
>>=:
1
2m >>= k = State $ \s -> let (a, s') = runState m s
in runState (k a) s'
总结

Writer、Reader、State。现在你已经将这三个强大的武器添加到你的兵器库了,请不遗余力地使用它们!
参考资料
英文原文链接: Three Useful Monads (Written by Aditya Bhargava)
原创作品,允许转载,转载时无需告知,但请务必以超链接形式标明文章原始出处(http://blog.forec.cn/2017/03/02/translation-adit-tum/) 、作者信息(Forec)和本声明。