哈斯克尔语中"<-"的含义



我试图理解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如何同时使用mn即使它的论点只有m

所以事实上我们在这里确实"先绑定n再绑定m"。以嵌套方式,就是这样。

最新更新