Haskell中do块中`< -`的含义

时间:2013-11-13 18:48:39

标签: haskell monads

我正在尝试理解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块中应用双参数函数。我怀疑这是一个卷曲的情况,但我对它究竟是如何工作有点模糊。

有人可以解释上述两个谜团吗?

4 个答案:

答案 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)

关注nponeccop's advice

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] 

函数hg内定义,即在g的参数范围内。这就是h使用mn的方式,即使只有m是其参数。

所以实际上我们确实“首先绑定n然后绑定m”。在nested fashion,即。