我试图理解Haskell的monads,阅读"好奇程序员的Monads"。我遇到了列表Monad的例子:
tossDie=[1,2,3,4,5,6]
toss2Dice = do
n <- tossDie
m <- tossDie
return (n+m)
main = print toss2Dice
我理解do
块生成 36 元素列表的方式m
- 它将n
的每个元素映射为 6 个元素的列表,然后连接这些列表。我不明白的是n
是如何因m <- tossDie
的存在而改变的,从 6 个元素列表到 36 个元素。显然,"我们先绑定n
,然后再绑定m
"在这里的理解是错误的,但什么是对的?
我也不完全清楚如何在do
块中应用双参数函数。我怀疑这是一个卷曲的情况,但我对它究竟是如何工作的有点模糊。
有人可以解释一下上述两个谜团吗?
对于列表(如 tossDie
),do
表示法的作用类似于列表推导 - 也就是说,好像每个变量绑定都是一个嵌套的foreach
循环。
执行块表达式:
toss2Dice = do { n <- tossDie; m <- tossDie; return (n+m) }
执行与此列表理解相同的操作:
toss2Dice = [ n+m | n <- tossDie, m <- tossDie ]
结果与以下命令式伪代码相当:
toss2Dice = []
foreach n in tossDie:
foreach m in tossDie:
toss2Dice.push_back(n+m)
除了Haskell的例子懒惰地,按需生成结果,而不是急切地一次性生成结果。
如果你查看列表的monad实例,你可以看到它是如何工作的:
instance Monad [] where
xs >>= f = concat (map f xs)
return x = [x]
从do
块的开头开始,每个变量绑定都会在块的其余部分创建一个循环:
do { n <- tossDie; m <- tossDie; return (n+m) }
===> tossDie >>= n -> do { m <- tossDie; return (n+m) }
===> concat ( map (n -> do { m <- tossDie; return (n+m) }) tossDie )
请注意,map
函数循环访问列表tossDie
中的项,并且结果concat
。 映射函数是do
块的其余部分,因此第一个绑定有效地在其周围创建了一个外部循环。
其他绑定创建连续嵌套的循环;最后,return
函数从每个计算值(n+m)
创建一个单例列表,以便"绑定"函数>>=
(需要列表)可以正确连接它们。
我假设的有趣之处在于:
toss2Dice = do
n <- tossDie
m <- tossDie
return (n+m)
这在某种程度上等同于以下 Python:
def toss2dice():
for n in tossDie:
for m in tossDie:
yield (n+m)
当涉及到列表 monad 时,您可以将 do 表示法中的绑定箭头 ( <-
) 视为传统的命令式 "foreach" 循环。之后的一切
n <- tossDie
属于该 foreach 循环的"循环体",因此将针对分配给n
的每个值tossDie
计算一次。
如果你想从do
符号到实际的绑定运算符>>=
脱糖,它看起来像这样:
toss2Dice =
tossDie >>= (n ->
tossDie >>= (m ->
return (n+m)
)
)
注意"内环体"是如何
(n ->
tossDie >>= (m ->
return (n+m)
)
)
将针对 tossDie
中的每个值执行一次。这几乎等同于嵌套的Python循环。
技术上的笨拙:你从绑定箭头中获得"foreach"循环的原因与你正在使用的特定monad有关。箭头对于不同的单子意味着不同的东西,要知道它们对特定的单子意味着什么,你必须做一些侦查并弄清楚这个单子是如何工作的。
箭头被脱糖成对绑定运算符 >>=
的调用,这对不同的 monads 也有不同的工作方式——这就是绑定箭头对不同 monads <-
也不同工作的原因!
在列表 monad 的情况下,绑定运算符>>=
向左获取一个列表,向右获取一个返回列表的函数,并将该函数应用于列表的每个元素。如果我们想以一种繁琐的方式将列表中的每个元素加倍,我们可以想象这样做是
λ> [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 之间的自然数。
列表 monad 以这种方式工作的原因是绑定运算符的这种特殊行为>>=
和return
使 monad 定律成立。monad 定律对我们(也许还有喜欢冒险的编译器)很重要,因为它们让我们以我们知道不会破坏任何东西的方式更改代码。
这样做的一个非常可爱的副作用是,它使列表非常方便地表示值的不确定性:假设您正在构建一个 OCR thingey,它应该查看图像并将其转换为文本。您可能会遇到可能是 4 或 A 或 H 的字符,但您不确定。通过让 OCR 在列表 monad 中工作并返回列表['A', '4', 'H']
您已经覆盖了您的基础。实际上,使用扫描的文本变得非常简单易读do
列表monad的符号。(看起来您正在使用单个值,而实际上您只是在生成所有可能的组合!
@kqr答案:
列表 monad 的>>=
实际上是 concatMap
,一个将元素映射到元素列表并连接列表的函数,但参数翻转:
concatMap' x f = concat (map f x)
或者
concatMap' = flip concatMap
return
只是
singleElementList x = [x]
现在我们可以用 concatMap'
和 singleElementList
替换 >>=
:
toss2Dice =
concatMap' tossDie (n ->
concatMap' tossDie (m ->
singleElementList (n+m)
)
)
现在我们可以用它们的主体替换 2 个函数:
toss2Dice =
concat (map (n ->
concat (map (m ->
[n+m]
) tossDice)
) tossDice)
删除多余的换行符:
toss2Dice = concat (map (n -> concat (map (m -> [n+m]) tossDice)) tossDice)
或更短的concatMap
:
toss2Dice = concatMap (n -> concatMap (m -> [n+m]) tossDice) tossDice
遵循NPONECCOP的建议,与
for = flip concatMap
您的代码变为
toss2Dice =
for {- n in -} tossDie {- call -}
(n-> for {- m in -} tossDie {- call -}
(m-> [n+m]))
很明显,我们有嵌套函数,一个在另一个内部;所以内部函数(m-> [n+m])
,在外部函数的参数n
范围内,可以访问它(到参数n
即)。因此,它使用外部函数的参数值,该值在每次调用内部函数时都是相同的,但是在外部函数的同一调用期间调用了多少次。
这可以用命名函数重写,
toss2Dice =
for {- each elem in -} tossDie {- call -} g
where g n = for {- each elem in -} tossDie {- call -} h
where h m = [n+m]
函数h
是在g
内部定义的,即在g
参数的作用域中。这就是h
如何同时使用m
和n
即使它的论点只有m
。
所以事实上我们在这里确实"先绑定n
再绑定m
"。以嵌套方式,就是这样。