folds 的不同之处似乎是一个常见的混淆来源,所以这里有一个更一般的概述:
考虑用一些函数f 和种子z 折叠一个包含n 个值的列表[x1, x2, x3, x4 ... xn ]。
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。它也是惰性的,但是因为它运行forwards,f 的每个应用程序都添加到结果的inside。因此,为了计算结果,Haskell 构造了一个 single 函数应用程序,其第二个参数是折叠列表的其余部分。如果f 在它的第二个参数中是惰性的——例如,一个数据构造函数——结果将是incrementally惰性,只有当部分结果需要时才计算折叠的每一步它被评估了。
所以我们可以看到为什么 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 wiki 也有 a page discussing this。