我正在尝试理解Haskell monad,阅读"Monads for the Curious Programmer"。我遇到了List Monad的例子:
tossDie=[1,2,3,4,5,6]
toss2Dice = do
n <- tossDie
m <- tossDie
return (n+m)
main = print toss2Dice
do
块生成m
作为36元素列表的方式我理解 - 它将n
的每个元素映射为6个元素的列表,然后连接这些列表。我不明白的是n
是如何因m <- tossDie
的存在而改变的,从6个元素列表到36个元素。显然“我们先绑定n
然后绑定m
”这里的理解是错误的,但是什么是对的?
我还不完全清楚如何在do
块中应用双参数函数。我怀疑这是一个卷曲的情况,但我对它究竟是如何工作有点模糊。
有人可以解释上述两个谜团吗?
答案 0 :(得分:10)
我认为有趣的是:
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 notation中查看绑定箭头(<-
)作为传统命令&#34; foreach&#34;循环。
n <- tossDie
属于&#34;循环体&#34;该foreach循环的结果,对tossDie
分配给n
的每个值都将进行一次评估。
如果你想要从do
符号到实际绑定操作符>>=
,它看起来像这样:
toss2Dice =
tossDie >>= (\n ->
tossDie >>= (\m ->
return (n+m)
)
)
注意&#34;内部循环体&#34;
(\n ->
tossDie >>= (\m ->
return (n+m)
)
)
对tossDie
中的每个值执行一次。这几乎等同于嵌套的Python循环。
技术mumbo-jumbo:你得到的原因&#34; foreach&#34;绑定箭头中的循环与您正在使用的特定monad有关。箭头对于不同的monad来说意味着不同的东西,并且要知道它们对于特定monad的意义,你必须做一些调查并弄清楚monad是如何工作的。
箭头被调用到绑定操作符>>=
的调用中,对于不同的monad也有不同的工作方式 - 这就是绑定箭头<-
对不同的monad也有不同的工作原因!
在列表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之间的自然数。
list monad以这种方式工作的原因是绑定运算符>>=
和return
的这种特殊行为使得monad定律成立。 monad法则对我们(也许是冒险的编译器)很重要,因为它们让我们以我们知道不会破坏任何东西的方式改变代码。
这个非常可爱的副作用是它使得列表非常方便地表示值的不确定性:假设您正在构建一个OCR thingey,它应该查看图像并将其转换为文本。您可能会遇到一个可能是4或A或H的角色,但您不确定。通过让OCR thingey在列表monad中工作并返回列表['A', '4', 'H']
,您已经覆盖了基础。实际上使用扫描的文本然后使用do
表示monad的表示法变得非常容易和可读。 (它看起来像你正在使用单个值,而实际上你只是生成所有可能的组合!)
答案 1 :(得分:10)
对于列表(例如tossDie
),do
表示法就像列表解析一样 - 也就是说,好像每个变量绑定都是嵌套的foreach
循环。 / p>
do-block表达式:
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)
中创建单个列表,以便“绑定”函数>>=
(需要列表)可以正确地连接它们。
答案 2 :(得分:4)
添加到@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)
)
)
现在我们可以用它们的身体替换这两个函数:
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
答案 3 :(得分:3)
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
”。在nested fashion,即。