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,并开始考虑使用 foldl 和 foldr。它们真的很像 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,它正好适合“向后”折叠Set
、Seq
等。例如,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的主要内容,如果未能解决你的问题,请参考以下文章