【问题标题】:foldl versus foldr behavior with infinite listsfoldl 与具有无限列表的 foldr 行为
【发布时间】:2023-04-09 18:53:01
【问题描述】:

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

我用 foldl 重写了它:

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

(请注意,step 函数的参数正确反转。)

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

我试图跟踪函数的执行,如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

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

【问题讨论】:

    标签: haskell lazy-evaluation combinators fold


    【解决方案1】:

    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。

    【讨论】:

    • 我来这里是因为我很好奇为什么 foldr 在 Haskell 中比 foldl 好,而在 Erlang 中则相反(我在 Haskell 之前学到的)。由于 Erlang 不是惰性的并且函数不是 curried,所以 Erlang 中的 foldl 的行为类似于上面的 foldl'。这是一个很好的答案!干得好,谢谢!
    • 这主要是一个很好的解释,但我发现foldl 的描述为“向后”,foldr 的描述为“向前”有问题。这部分是因为 flip 被应用到 (:) 以说明为什么折叠是向后的。自然的反应是,“当然是倒退:你flipped 列表串联!”看到所谓的“向后”也很奇怪,因为foldl 在完整评估中首先(最里面)将f 应用于第一个列表元素。 foldr 会“向后运行”,首先将f 应用于最后一个元素。
    • @DaveAbrahams:在foldl 和foldr 之间,忽略严格性和优化,首先意味着“最外层”,而不是“最里面”。这就是为什么foldr 可以处理无限列表而foldl 不能——右折叠首先将f 应用于第一个列表元素和折叠尾部的(未计算的)结果,而左折叠必须遍历评估f的最外层应用程序的整个列表。
    • 我只是想知道是否有任何情况下 foldl 比 foldl 更受欢迎,你认为有吗?
    • 很好的答案,但我认为将 foldl 的左关联表示为:f (foldl f [] [x1, x2,... xn-1]) xn 并将 foldr 的左关联表示为 f x1 (foldr f [] [x2, x3, ... xn]) 更清楚。在这个表示中,很明显 foldr 可以处理无限列表。
    【解决方案2】:
    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 总是在“外部”或“左侧”,所以它首先被扩展。无止境。

    【讨论】:

      【解决方案3】:

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

      【讨论】:

      • 所以“诀窍”是如果f 有时可以在看到第一个参数的值后返回而无需查看第二个参数,那么foldr 会提前退出(因为对于foldr,f 的第二个参数将是另一个foldr 调用的结果)。而无论f 做什么,foldl 都会立即递归。
      【解决方案4】:

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

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

      【讨论】:

      • 由于懒惰,在 Haskell 中有所不同。
      猜你喜欢
      • 2010-09-27
      • 2015-03-07
      • 1970-01-01
      • 1970-01-01
      • 2015-02-25
      • 2011-08-28
      • 2015-01-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多