我正在学习Haskell的基础知识,并且遇到很多教程说如果使用++从左到右构建列表比从右到左更有效。但我似乎无法理解为什么。
例如,为什么这个
a ++ (b ++ (c ++ (d ++ (e ++ f))))
比
更有效((((a ++ b) ++ c) ++ d) ++ e) ++ f
答案 0 :(得分:10)
一个简短的回答是,它不那么懒惰。另一个是它不需要的工作。
Haskell是懒惰的,所以如果你想了解操作特性,你必须检查解构下的值。或者,从技术上讲,我们应该使用此列表来了解它的表现。让我们在一个更简单的例子中计算take 2
。
take 3 ([1] ++ ([2] ++ [3])) vs. take 3 (([1] ++ [2]) ++ [3])
我们将在每次案例检查后不断更新定义,以了解减少的执行情况。为了减少混乱,我不会展开整个定义,而只是逐步完成它们强制的参数减少。为了完整起见,以下是take
和(++)
take 0 _ = []
take n [] = []
take n (x:xs) = x : take (n-1) xs
[] ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)
我们注意到,对于n > 0
,take n
检查其下一个参数,而(++)
始终只检查其第一个参数。
take 3 ([1] ++ ([2] ++ [3]))
take 3 (1 : ([] ++ ([2] ++ [3])))
1 : take 2 ([] ++ ([2] ++ [3]))
1 : take 2 ([2] ++ [3])
1 : take 2 (2 : ([] ++ [3]))
1 : 2 : take 1 ([] ++ [3]) -- *
1 : 2 : take 1 [3]
1 : 2 : take 1 (3 : [])
1 : 2 : 3 : take 0 []
1 : 2 : 3 : []
正如此操作显示的那样,take
能够在(++)
计算后立即消耗掉它们的值,只需两步即可返回结果的头部。
比较其他关联性。直到第3步,它才产生结果头。更深的嵌套会表现得更糟。
take 3 (([1] ++ [2]) ++ [3])
take 3 ((1 : ([] ++ [2])) ++ [3])
take 3 (1 : (([] ++ [2]) ++ [3]))
1 : take 2 (([] ++ [2]) ++ [3]) -- *
1 : take 2 ([2] ++ [3]
1 : take 2 (2 : ([] ++ [3]))
1 : 2 : take 1 ([] ++ [3]) -- *
1 : 2 : take 1 [3]
1 : 2 : take 1 (3 : [])
1 : 2 : 3 : take 0 []
1 : 2 : 3 : []
在这里,我们看到take
被迫提升评估左嵌套调用的结果“直到顶部”,然后才能返回部分结果。 (++)
更多的嵌套和更长的左手参数加剧了这种提升。同样值得注意的是,在星号步骤中我们在左侧附加一个空列表。在后一个示例中,这在中间步骤中发生为([] ++ [2]) ++ [3]
,而在前一个示例中,这在整个计算中发生为[] ++ ([2] ++ [3])
,例证了对嵌套附加的LHS进行的额外的,不必要的工作。
答案 1 :(得分:10)
归结为列表和++
的实现方式。您可以将列表视为
data List a = Empty | Cons a (List a)
只需将[]
替换为Empty
,将:
替换为Cons
。这是Haskell中单链表的非常简单的定义。单链表的连接时间为O(n)
,n
为第一个列表的长度。要理解为什么,请回想一下,对于链接列表,您持有对头部或第一个元素的引用,并且为了执行任何操作,您必须沿着列表向下走,检查每个值以查看它是否有后继。
因此,对于每个列表连接,编译器必须遍历第一个列表的整个长度。如果您有长度为a
,b
,c
的{{1}},d
,n1
和n2
列表,和n3
分别为表达式
n4
首先向下((a ++ b) ++ c) ++ d
构建a
,然后将此结果存储为a ++ b
,自x
n1
a
后采取n1
步骤元素。你离开了
(x ++ c) ++ d
现在,编译器向下x
构建x ++ c
,然后将此结果作为y
步骤存储在n1 + n2
步骤中(它必须向下移动a
的元素这次和b
。你和
y ++ d
现在y
走下来执行连接,执行n1 + n2 + n3
步骤,共计n1 + (n1 + n2) + (n1 + n2 + n3) = 3n1 + 2n2 + n3
步。
表达式
a ++ (b ++ (c ++ d))
编译器从内部括号开始,c ++ d -> x
步骤中构造n3
,导致
a ++ (b ++ x)
然后在b ++ x -> y
步骤中n2
,结果
a ++ y
最终在n1
步骤中折叠了哪些步骤,步骤总数为n3 + n2 + n1
,肯定少于3n1 + 2n2 + n3
。
答案 2 :(得分:0)
以下是我的理解:
在第一种情况下,
a ++ (b ++ (c ++ (d ++ (e ++ f))))
当你想要做一个++ b时,你已经构建了一个,你需要的只是将b附加到a。同样适用于++ b ++ c,您只需将c附加到++ b的结果,依此类推。
在第二种情况下,
((((a ++ b) ++ c) ++ d) ++ e) ++ f
如果你想做一个++ b ++ c,你必须首先做一个++ b,然后追加c,做一个++ b ++ c ++ d,你&#39 ; d必须再做一次++ b!然后a ++ b ++ c,然后追加d。
因此在第二种情况下会有很多重复计算,因此速度较慢。