Haskell 的 foldr/l 和 Clojure 的 reduce

Posted

技术标签:

【中文标题】Haskell 的 foldr/l 和 Clojure 的 reduce【英文标题】:Haskell's foldr/l and Clojure's reduce 【发布时间】:2019-07-18 09:13:02 【问题描述】:

我决定认真对待 Haskell,并开始考虑使用 foldlfoldr。它们真的很像 Clojure 的 reduce - 但我可能错了,很快就遇到了问题,我希望有人能轻松解释一下。

使用此文档: https://wiki.haskell.org/Foldr_Foldl_Foldl'

在深入实施我自己的 foldr/foldl 版本之前,我决定先测试 Prelude 中的现有版本:

± |master U:2 ✗| → ghci
GHCi, version 8.6.3: http://www.haskell.org/ghc/  :? for help
Loaded GHCi configuration from /Users/akarpov/.ghc/ghci.conf
Prelude> foldr (+) 0 [1..9999999]
49999995000000
Prelude> foldr (+) 0 [1..99999999]
*** Exception: stack overflow

没看到(使用 foldl 时结果相同);我宁愿期待一些类似于 Clojure 的东西:

> (time (reduce +' (range 1 99999999)))
"Elapsed time: 3435.638258 msecs"
4999999850000001

唯一明显(且不相关)的区别是使用 +' 而不是 +,但这只是为了适应 JVM 的类型系统 - 生成的数字不适合 [默认] Long,并且 +' 将自动使用需要时使用 BigInteger。最重要的是,没有堆栈溢出。 因此,这似乎表明 Haskell/Clojure 中的折叠/归约要么实现方式非常不同,要么我对 haskell 实现的使用是错误的。

如果相关,这些是全局项目设置: - 包:[] - 解析器:lts-13.8

【问题讨论】:

使用Data.List.foldl'foldl的严格形式。 Haskell 等效于 Clojure 的 (reduce +' (range 1 99999999))foldl' (+) 0 [1..99999998] (原文如此)(显然;得到与您显示的相同的结果)。 Haskell 范围包括在内。 另见Does Haskell have tail-recursive optimization?。这个问题的措辞不同,但实际上这个问题几乎是重复的——foldl 是尾递归的,等等。 【参考方案1】:

问题

正如the Wiki 解释的那样,函数(+) 的两个参数都是严格的,这意味着当您尝试执行1 + (2 + 3) 时,您首先需要计算(2 + 3)。虽然一开始这似乎不是一个大问题,但当你有一个很长的列表时就会出现问题。引用维基,

评估: 1 + (2 + (3 + (4 + (...)))), 1 被压入栈中。

然后:2 + (3 + (4 + (...))) 被评估。所以 2 被压入堆栈。

然后:评估3 + (4 + (...))。所以 3 被压入堆栈。

然后:4 + (...) 被评估。所以 4 被压入堆栈。

然后:当您评估足够大的 (+) 链时,您有限的堆栈最终会用完。这会触发堆栈溢出异常。

我不太了解 Clojure,但如果 (+') 有效,那么它肯定不需要在归约之前评估它的两个参数,这也是 Haskell 中的解决方案。

解决方案

Foldl 不能解决问题,因为众所周知 foldl 在返回结果之前必须遍历整个列表两次——效率不高——即使这样 (+) 仍然是严格的,所以可约表达式没有减少。

要解决这个问题,您必须使用非严格函数。在标准 Prelude 中,seq :: a -> b -> b 可以完全用于此目的,这就是 foldl' 的工作原理。

再次引用维基,

foldr 不仅是正确的折叠,它也是 最常见的正确折叠 fold 以使用,尤其是在转换列表(或其他 foldables)以相同的顺序放入具有相关元素的列表中。 值得注意的是,foldr 甚至可以有效地转换无限列表 进入其他无限列表。出于此类目的,它应该是您的第一个 和最自然的选择。例如,请注意 foldr (:) []==id。

foldl' 的问题在于它反转了列表。如果你有一个没有问题的交换函数,那么如果你的列表是有限的(记住 foldl 必须遍历它),foldl' 通常更好。另一方面,如果您的函数由于某些原因不一定需要整个列表,或者列表可能是无限的,请选择 foldr

【讨论】:

虽然我同意你的最终解决方案——即使用foldl'——但我认为你周围的解释中的很多事情都不太正确。问题不仅在于+ 的两个参数都很严格,而且foldr懒惰的;你在 Clojure 中说 +' 的两个参数都不能严格这一事实没有任何意义,因为 Clojure 不是一种惰性编程语言。在 Haskell 中出现问题的原因是由于创建了 thunk,但在 Clojure 中不会发生这种情况,因为它的 reduce 本质上是 Haskell 的 foldl';也就是说,它的累加器是严格的。 此外,您写道“foldl 在返回结果之前必须遍历整个列表两次”,这是不正确的。 foldl 在返回结果之前确实必须遍历整个列表,但它只需要执行一次。这与foldr 形成对比,如果累积函数在其第二个参数中是惰性的,则在返回结果之前甚至不必遍历整个列表一次。但是,在累积函数很严格的情况下,这两件事都无关紧要,因为无论如何都必须遍历整个列表。 @luqui, foldl 基本上对列表没有用处。 OTOH,它正好适合“向后”折叠SetSeq 等。例如,foldl (flip (:)) [] 将生成Set 元素的降序列表或将序列转换为反向列表。这同样适用于foldr':它对列表毫无价值,但有时对其他结构很有用。 @dfeuer,哦,Foldable 一个。我还没有将这些功能的地图更新为Foldable——我想现在已经有一段时间了。但是,是的,问题似乎在于折叠关联性与列表的关联性不兼容; foldl 非常适合 data Tsil a = Lin | Snoc [a] a。太棒了,我没有意识到这一点! @AlexisKing foldl 遍历列表“两次”,因为它遍历它一次以构建 thunk 结果,然后 that thunk 也被“遍历”,进行评估,展开堆栈,然后将其折叠回来(可以看作是第三次“遍历”)。无论如何,这是我的理论。

以上是关于Haskell 的 foldr/l 和 Clojure 的 reduce的主要内容,如果未能解决你的问题,请参考以下文章

Haskell (:) 和 (++) 的区别

Haskell 多态性和类型类实例

Haskell代码编程

文件处理-Haskell

Haskell 和函数组合

阅读和学习的好 Haskell 源 [关闭]