使用foldl'实现concatMap的性能提升有限列表?

时间:2017-03-26 18:50:50

标签: performance haskell functional-programming fold

我从Foldr Foldl Foldl'读到foldl'因为严格属性而对长有限列表更有效。我知道它不适合无限列表。

因此,我只限制长期有限列表的比较。

concatMap

concatMap是使用foldr实现的,这会让它变得懒惰。但是,根据文章,使用长有限列表将构建一个长的未减少的链。

concatMap :: Foldable t => (a -> [b]) -> t a -> [b]
concatMap f xs = build (\c n -> foldr (\x b -> foldr c b (f x)) n xs)

因此,我使用foldl'来提出以下实现。

concatMap' :: Foldable t => (a -> [b]) -> t a -> [b]
concatMap' f = reverse . foldl' (\acc x -> f x ++ acc) []

测试

我已经构建了以下两个函数来测试性能。

lastA = last . concatMap (: []) $ [1..10000]
lastB = last . concatMap' (: []) $ [1..10000]

然而,我对结果感到震惊。

lastA:
(0.23 secs, 184,071,944 bytes)
(0.24 secs, 184,074,376 bytes)
(0.24 secs, 184,071,048 bytes)
(0.24 secs, 184,074,376 bytes)
(0.25 secs, 184,075,216 bytes)

lastB:   
(0.81 secs, 224,075,080 bytes)
(0.76 secs, 224,074,504 bytes)
(0.78 secs, 224,072,888 bytes)
(0.84 secs, 224,073,736 bytes)
(0.79 secs, 224,074,064 bytes)

后续问题

concatMap在时间和记忆上都超过了我的concatMap'。我想知道我在concatMap'实施中犯了错误。

因此,我怀疑陈述foldl'的善意的文章。

  1. concatMap中是否有任何黑魔法使其如此高效?

  2. 对于长有限列表,foldl'是否更有效?

  3. 使用带有长有限列表的foldr是否会构建一个长的未减少的链并影响性能?

1 个答案:

答案 0 :(得分:6)

  

concatMap中是否有任何黑魔法使其如此高效?

不,不是真的。

  

对于长有限列表,foldl'是否更有效?

并非总是如此。这取决于折叠功能。

关键是,foldlfoldl'始终必须在生成输出之前扫描整个输入列表。相反,foldr并非总是如此。

作为极端情况,请考虑

foldr (\x xs -> x) 0 [10..10000000]

立即求值为10 - 仅评估列表的第一个元素。减少类似于

foldr (\x xs -> x) 0 [10..10000000]
= foldr (\x xs -> x) 0 (10 : [11..10000000])
= (\x xs -> x) 10 (foldr (\x xs -> x) 0 [11..10000000])
= (\xs -> 10) (foldr (\x xs -> x) 0 [11..10000000])
= 10

并且由于懒惰而无法评估递归调用。

通常,在计算foldr f a xs时,在评估f y ys之前检查ys是否能够构建输出的一部分非常重要。例如

foldr f [] xs
where f y ys = (2*y) : ys
在评估_ : _2*y之前,

会生成一个列表单元格ys。这使其成为foldr的绝佳候选人。

同样,我们可以定义

map f xs = foldr (\y ys -> f y : ys) [] xs

运行得很好。它从xs消耗一个元素并输出第一个输出单元格。然后它消耗下一个元素,输出下一个元素,依此类推。在处理整个列表之前,使用foldl'不会输出任何内容,从而使代码效率非常低。

相反,如果我们写了

sum xs = foldr (\y ys -> y+ys) 0 xs

然后在xs的第一个元素被消耗之后我们不输出任何内容。 我们建立了一长串的thunk,浪费了大量的内存。 在这里,foldl'将在恒定的空间中工作。

  

使用带有长有限列表的foldr是否会构建一个长的未减少的链并影响性能?

并非总是如此。它在很大程度上取决于调用者如何消耗输出。

作为一个拇指规则,如果输出是" atomic",意味着输出消费者只能观察到它的一部分(例如Bool, Int, ...),那么它会更好使用foldl'。如果输出是"组成"许多独立值(列表,树,...)可能foldr是一个更好的选择,如果f可以逐步产生其输出,在流媒体"方式。