采用这两个C ++函数和示例用法:
vector<int> makeVect() {
vector<int> v = {1,2,3};
return v;
}
//usage
auto v = makeVect(); //vector is moved
void addFour(vector<int> &v) {
v.push(4);
}
//usage
addFour(v); //v is passed in as reference
在任何一种情况下都不会发生复制。这非常有效。
以下是相应的Haskell函数和用法:
makeVect :: (Num a) => [a]
makeVect = [1,2,3]
--usage
--Q1: is [1,2,3] copied or moved to v?
let v = makeVect
addFour :: (Num a) => [a] -> [a]
addFour xs = xs ++ [4]
--usage
--Q2: is the parameter copied or moved into the function?
--Q3: on return, is the result copied or moved out of the function?
addFour v
问题Q1,Q2,Q3在代码中。 Haskell移动或复制东西吗?有没有办法控制它?在这些情况下,Haskell与C ++相比效率如何?
答案 0 :(得分:13)
在Haskell中,值是不可变的。从概念上讲,复制或移动它们是没有意义的。全世界有多少副本5号?这是一个毫无意义的问题。 5只是是。每个人都可以自由使用它,它是相同的数字5.同样的事情[1,2,3]
。
如果我们查看典型编译器生成的代码,当然会有一些复制操作,但那些主要是指向不可变内存区域的指针。
答案 1 :(得分:7)
Q1:没有理由复制makeVect
,因为我们确定无法更改列表(列表是不可变的)。我期望编译器做的是使v
和makeVect
指向同一个列表。
Q2:Q1的推理相同,无需复制。
问题3:由于列表是不可变的,因此最后插入一个值需要复制。请注意,如果值已插入列表的开头,则无需复制。创建新列表后,可以将其返回给调用者,而无需复制。
当然这是所有实施细节;由于数据结构是不可变的,因此无论是复制还是共享,程序的行为都相同。由编译器决定什么是最好的,据我所知,没有办法控制它。
在效率方面,它取决于。有些操作可以在不需要复制任何内容的情况下完成(比如在列表的开头添加元素),因此它们的性能可与C ++相媲美。但是,其他操作需要复制原始数据结构的大部分(比如更改数组中的元素),而这些操作实际上要比它们的C ++对应物慢得多。这并不一定意味着在Haskell中没有针对某些问题的有效解决方案,它只是意味着C ++和Haskell是非常不同的语言,并且在C ++中运行良好的方法不一定是在Haskell中完成它的最佳方式。
答案 2 :(得分:6)
这是一个示意图,显示您如何期望在程序的各个阶段将值布置在内存中。
最初你写makeVect = [1, 2, 3]
。内存中的布局类似于
1 : 2 : 3 : []
// makeVect ----^
现在,如果你写x = makeVect
,你不需要复制任何内容 - 你可以告诉x
指向与当前点makeVect
相同的地方。
// x -----v
1 : 2 : 3 : []
// makeVect -----^
但是,当您编写addFour x
时,您需要一个以4
作为最终元素的列表。您目前唯一的列表有3
作为最终元素,因此您需要一个新列表
// x ----v
1 : 2 : 3 : []
// makeVect ----^
// addFour x ----v
1 : 2 : 3 : 4 : []
如果您没有将4
添加到列表中,而是先添加0
,则无需创建新列表。您可以使用自己的指针添加一个额外的内存位置,因此内存布局类似于
// x -------v
0 : 1 : 2 : 3 : []
// addZero x --^ ^--- makeVect
请注意,这些“内存布局”仅是示意图,并不代表实际内存位置。这取决于编译器和优化级别。这只是对理解的帮助,以及向您展示可能如何完成的方法。
答案 3 :(得分:1)
这不是一个完整的答案,只是关于类型的一个小警告。如果你在Haskell中有一个单态(无类型变量)顶级值,那么它几乎肯定只会被评估一次:
makeVect :: [Int]
makeVect = [1,2,3]
main = do
print makeVect
print makeVect
但是,你给出的例子是多态的。这意味着你可以用不同的类型要求它的两个不同版本,这使它更像是一个可以多次评估的函数:
makeVect :: (Num a) => [a]
makeVect = [1,2,3]
main = do
print (makeVect :: [Int])
print (makeVect :: [Double])
在这个版本中,两个makeVect
不可能在内存中引用相同的值,因为它们的类型不同。
(非常不喜欢的monomorphism restriction正是因为这种可以多次计算多态值的行为而被认为可能令人困惑。)