为什么Haskell列表在';他是左撇子
我正在学习Haskell的基础知识,并遇到许多教程说,如果使用++从左到右构造列表比从右到左更有效。但我似乎不明白为什么 比如,为什么会这样为什么Haskell列表在';他是左撇子,haskell,Haskell,我正在学习Haskell的基础知识,并遇到许多教程说,如果使用++从左到右构造列表比从右到左更有效。但我似乎不明白为什么 比如,为什么会这样 a ++ (b ++ (c ++ (d ++ (e ++ f)))) 它的效率比 ((((a ++ b) ++ c) ++ d) ++ e) ++ f 以下是我的理解: 在第一种情况下 a ++ (b ++ (c ++ (d ++ (e ++ f)))) 当你想做a++b时,a已经被构造好了,你所需要的只是把b附加到a上。对于a++b++c也是如此,
a ++ (b ++ (c ++ (d ++ (e ++ f))))
它的效率比
((((a ++ b) ++ c) ++ d) ++ e) ++ f
以下是我的理解: 在第一种情况下
a ++ (b ++ (c ++ (d ++ (e ++ f))))
当你想做a++b时,a已经被构造好了,你所需要的只是把b附加到a上。对于a++b++c也是如此,您只需要在a++b的结果中添加c,依此类推
而在第二种情况下,
((((a ++ b) ++ c) ++ d) ++ e) ++ f
当你想做a++b++c时,你必须先做a++b,然后附加c,要做a++b++c++d,你必须再做a++b!然后a++b++c,然后附加d
因此,在第二种情况下,有大量重复计算,因此速度较慢。一个简短的答案是,它不那么懒惰。另一个原因是它做了不必要的工作 Haskell是懒惰的,所以如果您想了解操作特性,就必须在解构下检查值。或者,从技术上讲,我们应该使用这个列表来了解它的性能。让我们计算一个简单的例子,以2为例
take 3 ([1] ++ ([2] ++ [3])) vs. take 3 (([1] ++ [2]) ++ [3])
我们将在每次案例检查后不断更新定义,以查看缩减的执行情况。为了减少混乱,我将不展开整个定义,而只是一步一步地介绍它们强制的参数缩减。为了完整起见,这里有一些关于take
和(++)
我们注意到,对于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
能够在(++)
的计算值可用时立即惰性地消耗这些值,只需两步就返回结果的开头
比较其他关联性。直到第三步,它才产生结果水头。更深层的筑巢效果会更差
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所做的额外的、不必要的工作。归结到如何实现列表和+
。您可以想象列表的实现方式如下
data List a = Empty | Cons a (List a)
只需将[]
替换为空,:
替换为Cons
。这是Haskell中单链表的一个非常简单的定义。单链表的串联时间为O(n)
,其中n
是第一个列表的长度。要理解原因,请回想一下,对于链接列表,您持有对头元素或第一个元素的引用,并且为了执行任何操作,您必须在列表中遍历,检查每个值以查看它是否有后续值
因此,对于每个列表串联,编译器必须遍历第一个列表的整个长度。如果列表中的a
、b
、c
和d
的长度分别为n1
、n2
、n3
和n4
,则对于表达式
((a ++ b) ++ c) ++ d
a ++ (b ++ (c ++ d))
它首先向下走a
构造a++b
,然后将该结果存储为x
,因为a
具有n1
元素,所以它采取n1
步骤。你只剩下
(x ++ c) ++ d
y ++ d
现在编译器向下遍历x
以构造x++c
,然后将此结果存储为y
中的n1+n2
步骤(这次它必须遍历a
和b
的元素)。你只剩下
(x ++ c) ++ d
y ++ d
现在向下走y
,执行级联,采取n1+n2+n3
步骤,总共n1+(n1+n2)+(n1+n2+n3)=3n1+2n2+n3
步骤
为了表达
((a ++ b) ++ c) ++ d
a ++ (b ++ (c ++ d))
编译器从内圆括号开始,在n3
步骤中构造c++d->x
,结果是
a ++ (b ++ x)
a ++ y
然后在n2
步骤中b++x->y
,导致
a ++ (b ++ x)
a ++ y
最终在n1
步骤中折叠,步骤总数为n3+n2+n1
,肯定少于3n1+2n2+n3
,谢谢。我认为你的回答和J.Abrahamson的结合很好地解释了这一点。