这是一个非常复杂的问题,因为 Haskell 和 GHC 有两个特性:
- 懒惰评估
- 列表融合
列表融合意味着在某些情况下,GHC 可以将列表处理代码重写为不分配列表单元的循环。因此,根据使用它的上下文,相同的代码不会产生额外的成本。
惰性求值意味着如果一个操作的结果没有被消费,那么你就不用支付计算它的成本。例如,这很便宜,因为您只需要构造列表的前十个元素:
example = take 10 ([1..1000000] ++ [1000001])
事实上,在该代码中,take 10 可以与列表追加融合,因此它与 [1..10] 相同。
但是让我们假设我们正在使用我们创建的所有列表的所有元素,并且编译器没有融合我们的列表操作。现在回答您的问题:
如果我将一个元素添加到 Haskell 中的列表中,Haskell 会返回一个(完全?)新列表,并且不会操作原始列表。现在假设我有一个包含一百万个元素的列表,并在末尾附加一个元素。 Haskell 是否“复制”整个列表(100 万个元素)并将元素添加到该副本中?或者是否有一个巧妙的“技巧”在幕后进行以避免复制整个列表?
存在避免复制整个列表的技巧,但是通过附加到其末尾,您可以击败它们。要理解的是,函数式数据结构通常被设计为“修改”它们的操作将利用 结构共享 来尽可能多地重用旧结构。例如,附加两个列表可以这样定义:
(++) :: [a] -> [a] -> [a]
[] ++ ys = ys
(x:xs) ++ ys = x : xs ++ ys
看这个定义,你可以知道列表ys 将在结果中被重用。因此,如果我们有 xs = [1..3]、ys = [4..5] 和 xs ++ ys,所有这些都被完全评估并同时保留在内存中,它看起来像这样的内存:
+---+---+ +---+---+ +---+---+
xs = | 1 | -----> | 2 | -----> | 3 | -----> []
+---+---+ +---+---+ +---+---+
+---+---+ +---+---+
ys = | 4 | -----> | 5 | -----> []
+---+---+ +---+---+
^
|
+------------------------------------+
|
+---+---+ +---+---+ +---+---+ |
xs ++ ys = | 1 | -----> | 2 | -----> | 3 | -------+
+---+---+ +---+---+ +---+---+
这就是这么说:如果您执行xs ++ ys,并且它没有融合,并且您消耗整个列表,那么这将创建xs的副本,但将内存重用于@987654335 @。
但是现在让我们再看看你的这个问题:
现在假设我有一个包含一百万个元素的列表,并且我在末尾附加了一个元素。 Haskell 是否“复制”整个列表(100 万个元素)并将元素添加到该副本中?
类似于[1..1000000] ++ [1000001],是的,它会复制整百万个元素。但另一方面,[0] ++ [1..1000000] 只会复制[0]。经验法则是这样的:
- 在列表的开头添加元素是最有效的。
- 在列表末尾添加元素通常效率低下,特别是如果您一遍又一遍地这样做。
这类问题的一般解决方案是:
- 修改您的算法,以便以列表有效支持的访问模式使用列表。
- 不要使用列表;使用其他一些序列数据结构来有效地支持您手头问题所需的访问模式。另一个答案提到了差异列表,但其他值得一提的是: