我读了Learn You a Haskell for Great Good!书的旧俄语翻译。我看到目前的英文版(在线版)比较新,所以我也是时候看一下。
当您将两个列表放在一起时(即使您附加单个列表 到列表,例如:
[1,2,3] ++ [4])
,内部, Haskell必须 浏览++
左侧的整个列表。那不是 处理不太大的列表时出现问题。 但是推杆 列表末尾的内容是五千万条长 需要一段时间。然而,把东西放在开头 使用:
运算符(也称为cons运算符)的列表是 瞬时的。
我假设 Haskell必须遍历整个列表以获取foldr
,foldr1
的列表的最后一项, scanr
和scanr1
函数。另外我假设 Haskell将为获取前一个元素(以及每个项目等)执行相同的操作。
但我发现自己错了:
UPD
我尝试使用此代码,我看到两种情况的处理时间相似:
data' = [1 .. 10000000]
sum'r = foldr1 (\x acc -> x + acc ) data'
sum'l = foldl1 (\acc x -> x + acc ) data'
每个Haskell列表都是双向的吗? 我假设首先获取列表Haskell的最后一项是迭代每个项目并记住必要的项目(例如最后一项)以获取(稍后)前一项双向列表(对于懒惰)计算)。我是对的吗?
答案 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),...仅在内存中保留最后一个结果(单个整数) )正在扫描列表。