这篇文章是 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)和本声明。