foldl与具有无限列表的foldr行为

时间:2010-06-21 05:33:24

标签: haskell lazy-evaluation combinators fold

this question中myAny函数的代码使用foldr。当谓词满足时,它会停止处理无限列表。

我用foldl重写了它:

myAny :: (a -> Bool) -> [a] -> Bool
myAny p list = foldl step False list
   where
      step acc item = p item || acc

(请注意,步骤函数的参数已正确反转。)

但是,它不再停止处理无限列表。

我试图跟踪Apocalisp's answer中的函数执行:

myAny even [1..]
foldl step False [1..]
step (foldl step False [2..]) 1
even 1 || (foldl step False [2..])
False  || (foldl step False [2..])
foldl step False [2..]
step (foldl step False [3..]) 2
even 2 || (foldl step False [3..])
True   || (foldl step False [3..])
True

但是,这不是函数的行为方式。这怎么回事?

4 个答案:

答案 0 :(得分:209)

fold的差异似乎经常引起混淆,所以这里有一个更概括的概述:

考虑使用某个函数[x1, x2, x3, x4 ... xn ]和种子f折叠n个值z的列表。

foldl是:

  • 左关联f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • 尾递归:它遍历列表,然后生成值
  • 懒惰:在需要结果之前不评估任何内容
  • 向后foldl (flip (:)) []撤消列表。

foldr是:

  • 右关联f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
  • 递归到参数:每次迭代都会将f应用于下一个值以及折叠列表其余部分的结果。
  • 懒惰:在需要结果之前不评估任何内容
  • 前锋foldr (:) []不会更改列表。

这里有一个稍微微妙的点,有时会引起人们的注意:由于foldl 向后f的每个应用都会添加到外部结果;并且因为它是 lazy ,所以在需要结果之前不会对任何内容进行评估。这意味着要计算结果的任何部分,Haskell首先遍历整个列表构建嵌套函数应用程序的表达式,然后计算最外层函数,将其参数计算为需要。如果f总是使用它的第一个参数,这意味着Haskell必须一直递归到最里面的术语,然后向后计算f的每个应用程序。

这显然与大多数功能程序员所熟悉和喜爱的有效尾递归相差甚远!

实际上,即使foldl在技术上是尾递归的,因为整个结果表达式是在评估任何内容之前构建的, foldl会导致堆栈溢出!

另一方面,请考虑foldr。它也很懒,但因为它运行前进f的每个应用程序都会添加到结果的内部中。因此,为了计算结果,Haskell构造了一个单个函数应用程序,其第二个参数是折叠列表的其余部分。如果f在其第二个参数(例如数据构造函数)中是惰性的,则结果将是递增延迟,折叠的每个步骤仅在结果的某些部分计算时需要它的是评估。

因此,当foldr没有时,我们可以看到为什么foldl有时会在无限列表上工作:前者可以懒惰地将无限列表转换为另一个惰性无限数据结构,而后者必须检查整个列表生成结果的任何部分。另一方面,foldr具有立即需要两个参数的函数,例如(+),与foldl非常相似(或者更确切地说,不起作用),之前构建一个巨大的表达式评估它。

因此需要注意的两点是:

  • foldr可以将一个懒惰的递归数据结构转换为另一个。
  • 否则,懒惰的折叠会因大型或无限列表上的堆栈溢出而崩溃。

您可能已经注意到,foldr可以完成foldl可以做的所有事情,还有更多。这是真的!事实上, foldl几乎没用!

但是如果我们想通过折叠大(但不是无限)列表来产生非惰性结果呢?为此,我们需要严格弃牌the standard libraries thoughfully provide

foldl'是:

  • 左关联f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • 尾递归:它遍历列表,然后生成值
  • 严格:每个功能应用程序都在评估
  • 向后foldl' (flip (:)) []撤消列表。

因为foldl'严格,要计算结果,Haskell将在每一步评估 f,而不是让左参数累积巨大的,无价值的表达。这给了我们想要的通常,有效的尾递归!换句话说:

  • foldl'可以有效地折叠大型列表。
  • foldl'将在无限列表中挂起无限循环(不会导致堆栈溢出)。

Haskell维基也有a page discussing this

答案 1 :(得分:26)

myAny even [1..]
foldl step False [1..]
foldl step (step False 1) [2..]
foldl step (step (step False 1) 2) [3..]
foldl step (step (step (step False 1) 2) 3) [4..]

直观地,foldl始终位于“外部”或“左侧”,因此首先展开。无限无限。

答案 2 :(得分:10)

你可以在Haskell的文档here中看到,foldl是尾递归的,如果传递一个无限列表将永远不会结束,因为它在返回一个值之前调用自己的下一个参数...

答案 3 :(得分:0)

我不知道Haskell,但在Scheme中,fold-right将始终在列表的最后一个元素上“行动”。因此对循环列表(与无限循环列表相同)不起作用。

我不确定fold-right是否可以写尾递归,但对于任何循环列表,你应该得到堆栈溢出。 fold-left OTOH通常使用尾递归实现,如果不及早终止,它将陷入无限循环。