【问题标题】:How is foldl lazy?foldl 是如何偷懒的?
【发布时间】:2011-09-20 15:08:24
【问题描述】:

在 Haskell 中有很多关于 foldlfoldrfoldl'good questions and answers

所以现在我知道了:
1) foldl 很懒
2) 不要使用foldl,因为它会炸毁堆栈
3)改用foldl',因为它很严格(ish

如何评估foldl
1) 创建了一大堆 thunk
2) Haskell 完成创建 thunk 后,thunk 减少
3) 如果 thunk 太多,则溢出堆栈

我感到困惑的是:
1)为什么在所有重击之后必须进行还原?
2) 为什么不像foldl' 那样评估foldl?这只是实现的副作用吗?
3) 从definition 来看,foldl 看起来可以使用尾递归有效地评估它——我如何判断一个函数是否真的会被有效地评估?如果我不想让我的程序崩溃,我似乎不得不开始担心 Haskell 中的评估顺序。

提前致谢。不知道我对foldl的评价理解是否正确——如有需要请指正。


更新:看来我的问题的答案与范式、弱范式和头部范式以及 Haskell 对它们的实现有关。
但是,我仍在寻找一个示例,其中更急切地评估组合函数会导致不同的结果(崩溃或不必要的评估)。

【问题讨论】:

  • foldl is 尾递归...但事实证明,在 Haskell 中(与 Eager 语言不同)尾递归是一件坏事。
  • @Daniel -- 好点,我写错了,并编辑了问题以更正它。另外,感谢您澄清这一点——我一直认为尾递归无疑是一件好事。
  • @DanielWagner Bang patterns 来救援!尾递归在 Haskell 中仍然是一件好事。
  • @DanBurton 是的,你打赌。如果你使用 Haskell 的 Eager 片段,那么尾递归当然也不错!
  • @MasterMastic 通常获得尾递归的方法是有一个累加器。将此与惰性结合起来,当您进行所有递归调用时,您会得到一个带有非常深嵌套的 thunk 的累加器。繁荣!堆栈溢出。

标签: haskell lazy-evaluation fold


【解决方案1】:

简短的回答是,在foldl f 中,f 不一定是严格的,因此可能过于急于减少预先的 thunk。但是,实际上通常是这样,所以您几乎总是希望使用foldl'

我写了a more in-depth explanation of how the evaluation order of foldl and foldl' works on another question。虽然有点长,但我认为它应该为您澄清一点。

【讨论】:

    【解决方案2】:

    你知道,根据定义:

    foldl op start (x1:x2:...:xN:[]) = ((start `op` x1) `op` x2) ...
    

    foldl 中执行此操作的行是:

    foldl op a (x:xs) = foldl op (a `op` x) xs
    

    你是对的,这是尾递归,但请注意表达式

    (a `op` x)
    

    是惰性的,在列表的末尾,将构建一个巨大的表达式,然后将其缩减。 foldl' 的区别只是上面的表达式在每次递归中都被强制计算,所以最后你有一个弱头范式的值。

    【讨论】:

    • 所以opfoldl 更懒惰?我想在我看来,foldl 有多个可能的惰性求值顺序,而 Haskell 选择了最差的一个。我会阅读这个“弱头范式”,希望它更有意义。
    • 为了保留语义, foldl 必须确实假设 op 是惰性的。请参阅 hammar 答案中的链接,那里解释得很好。顺便说一句,Haskell 选择的不是最差的,而是在所有情况下都是正确的。
    • 好吧,这更有意义。但是你所说的“在所有情况下都是正确的”是什么意思? Haskell 不会评估以后不使用的计算?
    • 没错。因为,当它这样做时,程序的语义就会改变。例如,考虑 (42, undefined) 是一个完美的元组。 Haskell 程序不能在绝对需要之前评估第二个组件而崩溃。
    【解决方案3】:

    我仍在寻找一个更急切地评估组合函数会导致不同结果的示例

    一般的经验法则是永远不要使用foldl。始终使用foldl'除非你应该使用foldr我认为你对foldl足够了解,了解为什么应该避免使用它。

    另请参阅:Real World Haskell > Functional Programming # Left folds, laziness, and space leaks

    但是,您的问题忽略了foldrfoldr 的妙处在于它可以产生增量结果,而 foldl' 需要遍历整个列表才能得出答案。这意味着foldr 的惰性允许它处理无限列表。还有一些关于这类事情的详细问题和答案。

    说到这里,让我试着简洁地回答你的问题。

    1) 为什么必须在所有重击之后进行归约?

    减少发生在严格点。例如,执行 IO 是一个严格点。 foldl' 使用 seq 添加一个额外的严格点,foldl 没有。

    2) 为什么不像 foldl' 那样评估 foldl?这只是实现的副作用吗?

    因为foldl'中的额外严格点

    3) 从定义来看, foldl 在我看来就像一个尾递归函数——我如何判断一个函数是否会被有效地评估?如果我不想让我的程序崩溃,我似乎不得不开始担心 Haskell 中的求值顺序。

    您需要了解更多关于惰性求值的知识。惰性求值并不是 Haskell 独有的,但 Haskell 是极少数默认使用惰性的语言之一。对于初学者,请记住始终使用foldl',就可以了。

    如果有一天懒惰真的给你带来了麻烦,那么你应该确保你了解懒惰和 Haskell 的严格点。您可能会说,上述理论日是惰性学习的严格点。

    另请参阅:PLAI > Part III: Laziness

    【讨论】:

    • 我没有提到foldr,因为一般来说,它并不等同。但是 +1 提到了严格点(这是一个实际的技术术语吗?管道和管子系列让我失望了)。
    • 我以为我是从PLAI那里捡来的,但又看了一遍,也许不是。我想我是从我们学习 PLAI 的大学课堂上得到的。这似乎是一个非常自然和技术性的术语。我很惊讶它没有更普遍。我将冒昧地创建一个关于它的 SO 问题,看看我们是否能提出一些好的解释。
    【解决方案4】:

    如果我不想让我的程序崩溃,我似乎不得不开始担心 Haskell 中的求值顺序。

    在我看来,如果您希望您的程序不崩溃,最好的选择是(根据所花费的每项努力的产出,从好到差排名): 1. 给它足够的资源。 2. 改进你的算法。 3. 做微优化(其中之一是foldl')。

    所以,与其担心评估的顺序,我宁愿首先担心要评估的什么foldr 够吗?我可以不用完全折叠吗?)。在此之前,我会增加可用的堆栈空间。

    您不会将整个程序限制为 8MB 的 RAM,对吗?那你为什么要限制堆栈空间呢?只需将堆栈空间增加到 4GB,然后在某些东西真正占用大量堆栈空间时开始担心(就像您对堆内存所做的那样)。

    并在某种程度上回答foldl 是如何懒惰的问题:

    foldl (\x y -> y) undefined [undefined, 8] -- evaluates to 8
    foldl' (\x y -> y) undefined [undefined, 8] -- fails to evaluate
    

    【讨论】:

    • 这个例子不是为了证明foldl的懒惰,而是为了演示需要懒惰的情况。
    • -1 建议您忽略明显的性能故障。给它足够的资源是 user 解决方案,而不是 programmer 解决方案。
    • @monadic,在大多数情况下完全使用列表是明显的性能故障,但这通常是可以接受的,甚至可以接受。
    • @Rotsor 不是真的。列表具有与数组不同的性能特征,但不一定不好。例如,前置 O(1) 就很棒。不要传播 FUD...
    • @monadic,可以使用比使用链表快得多的数组进行摊销 O(1) 前置附加。持久结构是一种需要付出代价的便利!懒惰也是一样。所以,如果你称“列表不好”FUD,我称“懒惰是坏”FUD。
    猜你喜欢
    • 2012-01-19
    • 1970-01-01
    • 2020-04-05
    • 1970-01-01
    • 2023-02-24
    • 2017-08-27
    • 1970-01-01
    • 2011-03-14
    • 1970-01-01
    相关资源
    最近更新 更多