具有无限列表的 foldl 与 foldr 行为 [英] foldl versus foldr behavior with infinite lists

查看:26
本文介绍了具有无限列表的 foldl 与 foldr 行为的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

The code for the myAny function in this question uses foldr. It stops processing an infinite list when the predicate is satisfied.

我使用 foldl 重写了它:

I rewrote it using foldl:

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

(注意 step 函数的参数是正确反转的.)

(Note that the arguments to the step function are correctly reversed.)

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

However, it no longer stops processing infinite lists.

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

I attempted to trace the function's execution as in 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

然而,这不是函数的行为方式.这是怎么回事?

However, this is not the way the function behaves. How is this wrong?

推荐答案

fold 的不同似乎经常引起混淆,所以这里有一个更一般的概述:

How folds differ seems to be a frequent source of confusion, so here's a more general overview:

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

Consider folding a list of n values [x1, x2, x3, x4 ... xn ] with some function f and seed z.

  • 左联想:f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • 尾递归:它遍历列表,然后产生值
  • 懒惰:在需要结果之前不进行任何评估
  • Backwards:foldl (flip (:)) [] 反转列表.
  • Left associative: f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • Tail recursive: It iterates through the list, producing the value afterwards
  • Lazy: Nothing is evaluated until the result is needed
  • Backwards: foldl (flip (:)) [] reverses a list.
  • 右结合:f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
  • 递归到参数中:每次迭代将 f 应用到下一个值和折叠列表其余部分的结果.
  • 懒惰:在需要结果之前不进行任何评估
  • Forwards:foldr (:) [] 返回未更改的列表.
  • Right associative: f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
  • Recursive into an argument: Each iteration applies f to the next value and the result of folding the rest of the list.
  • Lazy: Nothing is evaluated until the result is needed
  • Forwards: foldr (:) [] returns a list unchanged.

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

There's a slightly subtle point here that trips people up sometimes: Because foldl is backwards each application of f is added to the outside of the result; and because it is lazy, nothing is evaluated until the result is required. This means that to compute any part of the result, Haskell first iterates through the entire list constructing an expression of nested function applications, then evaluates the outermost function, evaluating its arguments as needed. If f always uses its first argument, this means Haskell has to recurse all the way down to the innermost term, then work backwards computing each application of f.

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

This is obviously a far cry from the efficient tail-recursion most functional programmers know and love!

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

In fact, even though foldl is technically tail-recursive, because the entire result expression is built before evaluating anything, foldl can cause a stack overflow!

另一方面,请考虑 foldr.它也是懒惰的,但是因为它运行forwardsf 的每个应用都被添加到结果的里面.因此,为了计算结果,Haskell 构造了一个 single 函数应用程序,其第二个参数是折叠列表的其余部分.如果 f 在它的第二个参数中是惰性的——例如一个数据构造函数——结果将是增量惰性,折叠的每一步只有当某些部分对需要它的结果进行评估.

On the other hand, consider foldr. It's also lazy, but because it runs forwards, each application of f is added to the inside of the result. So, to compute the result, Haskell constructs a single function application, the second argument of which is the rest of the folded list. If f is lazy in its second argument--a data constructor, for instance--the result will be incrementally lazy, with each step of the fold computed only when some part of the result that needs it is evaluated.

所以我们可以看到为什么 foldr 有时适用于无限列表而 foldl 不能:前者可以将无限列表懒惰地转换为另一个惰性无限数据结构,而后者必须检查整个列表以生成结果的任何部分.另一方面,带有立即需要两个参数的函数的 foldr,例如 (+),工作(或者更确切地说,不起作用)很像 foldl,在计算之前构建一个巨大的表达式.

So we can see why foldr sometimes works on infinite lists when foldl doesn't: The former can lazily convert an infinite list into another lazy infinite data structure, whereas the latter must inspect the entire list to generate any part of the result. On the other hand, foldr with a function that needs both arguments immediately, such as (+), works (or rather, doesn't work) much like foldl, building a huge expression before evaluating it.

所以要注意的两个要点是:

So the two important points to note are these:

  • foldr 可以将一种惰性递归数据结构转换为另一种.
  • 否则,惰性折叠会因大型列表或无限列表上的堆栈溢出而崩溃.
  • foldr can transform one lazy recursive data structure into another.
  • Otherwise, lazy folds will crash with a stack overflow on large or infinite lists.

您可能已经注意到,听起来 foldr 可以做 foldl 能做的所有事情,而且还有更多.这是真实的!事实上,foldl 几乎没用!

You may have noticed that it sounds like foldr can do everything foldl can, plus more. This is true! In fact, foldl is nearly useless!

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

But what if we want to produce a non-lazy result by folding over a large (but not infinite) list? For this, we want a strict fold, which the standard libraries thoughfully provide:

  • 左联想:f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • 尾递归:它遍历列表,然后产生值
  • 严格:每个功能应用程序都经过评估
  • Backwards:foldl' (flip (:)) [] 反转列表.
  • Left associative: f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • Tail recursive: It iterates through the list, producing the value afterwards
  • Strict: Each function application is evaluated along the way
  • Backwards: foldl' (flip (:)) [] reverses a list.

因为foldl'strict,Haskell会在每一步evaluate f来计算结果,而不是让左边的论点积累一个巨大的、未经评估的表达式.这为我们提供了我们想要的通常的、高效的尾递归!换句话说:

Because foldl' is strict, to compute the result Haskell will evaluate f at each step, instead of letting the left argument accumulate a huge, unevaluated expression. This gives us the usual, efficient tail recursion we want! In other words:

  • foldl' 可以有效地折叠大型列表.
  • foldl' 将挂在无限列表的无限循环中(不会导致堆栈溢出).
  • foldl' can fold large lists efficiently.
  • foldl' will hang in an infinite loop (not cause a stack overflow) on an infinite list.

Haskell wiki 也有一个讨论此内容的页面.

The Haskell wiki has a page discussing this, as well.

这篇关于具有无限列表的 foldl 与 foldr 行为的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆