三种实用 Monad

这篇文章是 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
2
half . half $ 8
=> 2

结果与预期一致。现在你决定记录这个函数的执行过程:

1
half x = (x `div` 2, "I just halved " ++ (show x) ++ "!")

看起来不错。如果我们想将其连续应用多次,又该怎么书写呢?我们无法直接使用 half . half $ 8,因为应用一次 half 的返回值已经变成了元组,我们无法对元组继续应用 half。下图展示了我们实际期望的功能:

显然这个功能不会自己产生,我们必须自己实现:

1
2
3
finalValue = (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
4
half :: Int -> Writer String Int
half x = do
tell ("I just halved " ++ (show x) ++ "!")
return (x `div` 2)

新的 half 会回传一个 Writer

runWriter 能帮助我们取出 Writer 封装的元组。

1
2
runWriter $ half 8
=> (4, "I just halved 8!")

然而,真正牛逼的地方在于,我们现在可以用 >>=half 串起来了:

1
2
runWriter $ 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
4
greeter :: 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
2
runReader 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 MonadReader Monad 最好的朋友:

她看起来和 Reader Monad 非常像,只不过它既可读又可写。这是 State 的定义:

1
State s a = State { runState :: s -> (a, s) }


你可以使用 get 获取状态,也可用 put 改变状态。举个例子:

1
2
3
4
5
6
7
8
greeter :: State String String
greeter = do
name <- get
put "tintin"
return ("hello, " ++ name ++ "!")

runState greeter $ "adit"
=> ("hello, adit!", "tintin")

没毛病!Reader 就像在说 “你无法改变我”,而 State 则对改变持兹瓷态度。
StateReader 的定义看起来非常相似:
return

1
return a = State $ \s -> (a, s)

>>=

1
2
m >>= k = State $ \s -> let (a, s') = runState m s
in runState (k a) s'

总结


WriterReaderState。现在你已经将这三个强大的武器添加到你的兵器库了,请不遗余力地使用它们!

参考资料


英文原文链接: Three Useful Monads (Written by Aditya Bhargava

原创作品,允许转载,转载时无需告知,但请务必以超链接形式标明文章原始出处(http://blog.forec.cn/2017/03/02/translation-adit-tum/) 、作者信息(Forec)和本声明。

分享到