似乎foldr
与列表理解进行了某种融合,因此与foldl
此示例中的(21mb)相比,它需要更少的内存(11mb)分配
myfunc = sum $ foldr g acc [ f x | x <- xs ]
f x = ..
g x y = ..
谁能解释一下如何以及为什么?懒惰的评估在这方面也有帮助。
似乎foldr
与列表理解进行了某种融合,因此与foldl
此示例中的(21mb)相比,它需要更少的内存(11mb)分配
myfunc = sum $ foldr g acc [ f x | x <- xs ]
f x = ..
g x y = ..
谁能解释一下如何以及为什么?懒惰的评估在这方面也有帮助。
本质上,我们可以将理解脱糖为map f xs
. 如果您正在编译它,那么 ghc 确实应该能够将总和、折叠和地图融合到一个通道中:http ://www.haskell.org/haskellwiki/Correctness_of_short_cut_fusion. 但即使你不是,那么懒惰也是你使用内存的朋友。map 生成的列表是惰性的—— f 仅在需要时应用。并且 f 只有当 foldr 需要它时才会被请求。并且由于您的 foldr 显然正在生成另一个(惰性)列表,因此折叠的每一步仅由 sum 依次要求。因此,您仍然可以依次应用每个函数,但您不需要一次生成完整的中间数据结构。当您编写了一组完整的函数组合时,评估模型将倾向于处理这组特定的代码,以一大堆挥手为模,有点像一个循环(尽管没有融合,一个相当数量的循环间接的)。
在遍历整个列表之前,左折叠不能产生任何输出(结果的一部分)。根据您折叠的功能,它可以构建一个大数据结构或一个大 thunk,它使用大量内存(如果您将例如 (+) 折叠在 列表上,它可以在恒定内存中运行Int
)。
对于适当的函数(这样可以在不检查第二个参数的情况下产生[部分]结果),正确的折叠可以增量地产生它们的结果,这样如果结果被适当地消耗并且输入列表被适当地生成,整个计算就可以运行在小的常数空间中。正如 sclv 所说,在这些情况下,它基本上简化为一个循环。
这是 GHC 编译器的一个特性。基本上,GHC 可以识别何时在“管道”中使用列表,并且可以将整个构造转换为while
C 中完全不分配列表的 -loop 的等价物。
这适用于foldr
而不是foldl
取决于g
您在示例中使用的功能的原因。因为foldr
,而不是foldl
,累积作为参数给出的函数的结果foldl
(又名:在开始实际评估函数之前需要整个列表g
,因此它建立了一个巨大的未评估函数“thunk”和list 作为它的结果——这就是为什么它在这种情况下使用更多内存的原因——虽然它foldr
可以在获得任何列表输入后立即开始评估g
),它在其累加器中被称为“严格”,并且可以做出某些假设可以导致优化的编译器。
例如,如果函数g
产生一个列表值,它可以继续前面提到的“管道”优化策略,基本上处理foldr
类似 amap
并将整个构造(从列表生成到列表消耗)变成一个严格的循环。这仅是可能的,因为foldr
它消耗的每个列表元素都会产生一个列表元素,这foldl
不能保证这样做(尤其是对于无限列表)。