为什么`foldr`,`foldr1`,`scanr`和`scanr1`函数在应用于大列表时具有生产效率的问题?

时间:2016-10-11 07:12:10

标签: haskell

我读了Learn You a Haskell for Great Good!书的旧俄语翻译。我看到目前的英文版(在线版)比较新,所以我也是时候看一下。

quote

  

当您将两个列表放在一起时(即使您附加单个列表   到列表,例如:[1,2,3] ++ [4]),内部, Haskell必须   浏览++左侧的整个列表。那不是   处理不太大的列表时出现问题。 但是推杆   列表末尾的内容是五千万条长   需要一段时间。然而,把东西放在开头   使用:运算符(也称为cons运算符)的列表是   瞬时的。

我假设 Haskell必须遍历整个列表以获取foldrfoldr1的列表的最后一项scanrscanr1函数。另外我假设 Haskell将为获取前一个元素(以及每个项目等)执行相同的操作。

但我发现自己错了:

UPD

我尝试使用此代码,我看到两种情况的处理时间相似:

data' = [1 .. 10000000]
sum'r = foldr1 (\x acc -> x + acc ) data' 
sum'l = foldl1 (\acc x -> x + acc ) data'

每个Haskell列表都是双向的吗? 我假设首先获取列表Haskell的最后一项是迭代每个项目并记住必要的项目(例如最后一项)以获取(稍后)前一项双向列表(对于懒惰)计算)。我是对的吗?

1 个答案:

答案 0 :(得分:6)

因为Haskell很懒,这很棘手。

评估head ([1..1000000]++[1..1000000])将立即返回1。列表永远不会在内存中完全创建:只有第一个列表的第一个元素才会出现。

如果您要求完整列表[1..1000000]++[1..1000000],那么++确实必须创建一个200万长的列表。

foldr可能会也可能不会评估完整列表。这取决于我们使用的功能是否是懒惰的。例如,这里是使用map f xs编写的foldr

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

这是有效的,因为map f xs是:列表单元格是按需流式生成的。如果我们只需要结果列表的前十个元素,那么我们确实只创建前十个单元格 - foldr将不会应用于列表的其余部分。如果我们需要完整的结果列表,则foldr将在完整列表中运行。

另请注意,xs++ys可以使用foldr

进行类似定义
foldr (:) ys xs

并具有类似的性能属性。

相比之下,foldl代替always runs over the whole list

在您提到的示例中,我们有longList ++ [something],附加到列表的末尾。如果我们要求的只是结果列表的第一个元素,那么这只需要花费一些时间。但是如果我们真的需要我们添加的最后一个元素,那么追加将需要遍历整个列表。这就是为什么最后追加被认为是O(n)而不是O(1)。

在上次更新中,问题涉及使用foldr运算符计算与foldl vs (+)的总和。在这种情况下,由于(+)是严格的(它需要两个参数来计算结果),因此两个折叠都需要扫描整个列表。在这种情况下的表现可以比较。实际上,他们将分别计算

1 + (2 + (3 + (4 + .....       -- foldr
(...(((1 + 2) + 3) +4) + ....  -- foldl 

通过比较,foldl'会提高内存效率,因为它会在构建上述巨型表达式之前开始减少上述总和。也就是说,它首先计算1+2(3),然后计算3+3(6),然后计算6 + 4(10),...仅在内存中保留最后一个结果(单个整数) )正在扫描列表。

对于OP:懒惰的话题第一次不容易掌握。这是相当广泛的 - 你刚刚遇到了大量不同的例子,这些例子有微妙但显着的性能差异。很难简洁地解释所有内容 - 它太宽泛了。我建议你专注于小例子并开始消化它们。