在Haskell中移动或复制(与C ++相比)

时间:2013-12-31 12:08:45

标签: c++ haskell

采用这两个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 ++相比效率如何?

4 个答案:

答案 0 :(得分:13)

在Haskell中,值是不可变的。从概念上讲,复制移动它们是没有意义的。全世界有多少副本5号?这是一个毫无意义的问题。 5只是。每个人都可以自由使用它,它是相同的数字5.同样的事情[1,2,3]

如果我们查看典型编译器生成的代码,当然会有一些复制操作,但那些主要是指向不可变内存区域的指针。

答案 1 :(得分:7)

Q1:没有理由复制makeVect,因为我们确定无法更改列表(列表是不可变的)。我期望编译器做的是使vmakeVect指向同一个列表。

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正是因为这种可以多次计算多态值的行为而被认为可能令人困惑。)