131

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

我用 foldl 重写了它:

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

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

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

我试图跟踪函数的执行,如Apocalisp 的回答

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

4 回答 4

248

fold不同之处似乎是一个常见的混淆来源,所以这里有一个更一般的概述:

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

foldl是:

  • 左联想f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • 尾递归:遍历列表,然后产生值
  • 懒惰:在需要结果之前不评估任何内容
  • Backwardsfoldl (flip (:)) []反转列表。

foldr是:

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

这里有一个稍微微妙的点有时会让人绊倒:因为foldl向后的,每个应用程序都f被添加到结果的外部;并且因为它是惰性的,所以在需要结果之前不会评估任何内容。这意味着要计算结果的任何部分,Haskell 首先遍历整个列表以构造嵌套函数应用程序的表达式,然后评估最外层的函数,并根据需要评估其参数。如果f总是使用它的第一个参数,这意味着 Haskell 必须一直递归到最里面的项,然后向后计算f.

这显然与大多数函数式程序员所熟知和喜爱的高效尾递归相去甚远!

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

另一方面,考虑foldr。它也是惰性的,但是因为它是向前运行的,所以每个应用程序f都会添加到结果内部。因此,为了计算结果,Haskell 构造了一个函数应用程序,其第二个参数是折叠列表的其余部分。如果f在它的第二个参数中是惰性的——例如,一个数据构造函数——结果将是递增的惰性,只有在评估需要它的结果的某些部分时才会计算折叠的每一步。

所以我们可以看到为什么foldr有时在无限列表foldl上工作而不能:前者可以将无限列表延迟转换为另一个延迟无限数据结构,而后者必须检查整个列表以生成结果的任何部分。另一方面,foldr对于一个同时需要两个参数的函数,例如(+), 工作(或者更确切地说,不工作)很像foldl,在评估它之前构建一个巨大的表达式。

因此,需要注意的两个重点是:

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

您可能已经注意到,这听起来像是foldr可以做任何事情foldl,甚至更多。这是真的!事实上,foldl 几乎没用!

但是如果我们想通过折叠一个大的(但不是无限的)列表来产生一个非惰性结果呢?为此,我们需要标准库精心提供的严格折叠

foldl'是:

  • 左联想f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • 尾递归:遍历列表,然后产生值
  • Strict:每个功能应用程序都在此过程中进行评估
  • Backwardsfoldl' (flip (:)) []反转列表。

因为foldl'strict,所以为了计算结果,Haskell 将在每一步求值 f,而不是让左参数累积一个巨大的、未求值的表达式。这给了我们想要的通常的、有效的尾递归!换句话说:

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

Haskell wiki 也有一个讨论这个的页面

于 2010-06-21T14:30:35.507 回答
28
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它总是在“外面”或“左边”,所以它首先被扩展。无穷无尽。

于 2010-06-21T05:54:41.283 回答
11

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

于 2010-06-21T05:38:42.330 回答
0

我不知道 Haskell,但在 Scheme 中,fold-right总是会首先对列表的最后一个元素“采取行动”。因此 is 不适用于循环列表(与无限列表相同)。

我不确定是否fold-right可以写成尾递归,但是对于任何循环列表,您都应该得到堆栈溢出。 fold-leftOTOH 通常使用尾递归实现,如果不提前终止它,它只会陷入无限循环。

于 2010-06-21T05:40:13.760 回答