我认为有趣的一点是:
toss2Dice = do
n <- tossDie
m <- tossDie
return (n+m)
这在某种程度上等同于以下 Python:
def toss2dice():
for n in tossDie:
for m in tossDie:
yield (n+m)
对于 list monad,您可以将<-
do 表示法中的绑定箭头 ( ) 视为传统的命令式“foreach”循环。之后的一切
n <- tossDie
属于该 foreach 循环的“循环体”,因此将对tossDie
分配给的每个值进行一次评估n
。
如果您想要从do
符号到实际绑定运算符的脱糖>>=
,它看起来像这样:
toss2Dice =
tossDie >>= (\n ->
tossDie >>= (\m ->
return (n+m)
)
)
注意“内循环体”如何
(\n ->
tossDie >>= (\m ->
return (n+m)
)
)
将为 中的每个值执行一次tossDie
。这几乎等同于嵌套的 Python 循环。
Technical mumbo-jumbo:从绑定箭头获得“foreach”循环的原因与您正在使用的特定 monad 有关。箭头对不同的 monad 有不同的含义,要知道它们对特定 monad 的含义,您必须进行一些调查并弄清楚该 monad 通常是如何工作的。
箭头被去糖化为对绑定运算符的调用>>=
,这对不同的 monad 也有不同的作用——这就是绑定箭头<-
对不同的 monad 也有不同作用的原因!
在 list monad 的情况下,bind 运算符>>=
将一个列表放在左边,一个函数返回一个列表到右边,然后将该函数应用于列表的每个元素。如果我们想以繁琐的方式将列表中的每个元素加倍,我们可以想象这样做
λ> [1, 2, 3, 4] >>= \n -> return (n*2)
[2,4,6,8]
(return
是使类型起作用所必需的。>>=
期望一个返回列表的函数,并且return
对于列表 monad,将值包装在列表中。) 为了说明一个可能更强大的示例,我们可以从想象函数开始
λ> let posneg n = [n, -n]
λ> posneg 5
[5,-5]
然后我们可以写
λ> [1, 2, 3, 4] >>= posneg
[1,-1,2,-2,3,-3,4,-4]
计算-4到4之间的自然数。
单子列表以这种方式工作的原因是绑定运算符的这种特殊行为>>=
并使return
单子法则成立。monad 法则对我们(也许还有冒险的编译器)很重要,因为它们让我们以我们知道不会破坏任何东西的方式更改代码。
这样做的一个非常可爱的副作用是它使列表非常方便地表示值的不确定性:假设您正在构建一个 OCR thingey,它应该查看图像并将其转换为文本。您可能会遇到可能是 4 或 A 或 H 的字符,但您不确定。通过让 OCR thingey 在 list monad 中工作并返回['A', '4', 'H']
您已经涵盖了基础的列表。do
实际使用扫描的文本然后使用list monad 的符号变得非常容易和可读。(看起来您正在使用单个值,而实际上您只是在生成所有可能的组合!)