功能编程:不变性应该走多远?

时间:2017-11-11 15:44:34

标签: functional-programming immutability

我意识到这可能是一个非常主观的问题,但我正在寻找那些比我在函数式编程方面更有经验的人的见解。

我的理解是,保持一切不可变的主要动力是让事情更容易理解,并阻止错误蔓延到并行任务。这样做的缺点是,每次要对数据结构进行更改时,都必须将整个数据结构复制到一个新对象中,但需要进行所需的更改。据推测,这样做有一些性能成本:虽然对于一个小对象我不会这么做三次,如果你正在处理大型矩阵或张量或类似的大型数据结构,那肯定会变得非常慢?

简而言之:

  1. 复制不可变数据结构是否存在性能损失,以及它有多重要?
  2. 如果是这样的话,你能举例说明你(个人)在制作不可变的内容和使其变为可变之间的界限吗?

3 个答案:

答案 0 :(得分:5)

  

我的理解是,保持一切不可变的主要动力是让事情更容易理解,并阻止错误进入并行任务。

这可能是主要动机。但还有其他优点。但它可以带来性能提升。例如, lockfree waitfree 数据结构是一种处理并行处理的方法,旨在减少锁定的开销。

  

这样做的缺点是,每次要对数据结构进行更改时,都必须将整个数据结构复制到一个新对象中,但需要进行所需的更改。

不正确。您无需复制整个数据结构。例如,指树是函数式编程中的典型数据结构。它在 O(d)(带有 d )<树>的深度中执行插入。所以它具有完全相同的性能。

使用不可变对象可以在很多/大多数情况下导致相同的时间复杂度,尽管算法通常以不同方式编写(例如,列表通常是链接列表,因此列表处理通常是为了避免随机索引查找)。

此外,数据结构不可变的事实也可以提高效率。例如,在许多编程语言中, strings 是不可变对象。某些编程语言具有实现 flyweight 模式的字符串存储。如果构造一个新字符串,它会检查字符串是否已经存在,如果是这种情况,则获取指向该字符串的指针。优点是内存占用减少了,而且由于可以缓存这些对象,它还可以提高性能。

  

会在制作不可变的内容和使其变为可变之间划清界线吗?

有像Haskell这样的编程语言所有是不可变的。在这种情况下,可变性通过使用monad完成,从而通过该过程传递新版本的对象。 Haskell编译器往往做得很好,有时可以达到命令式对应的速度。

答案 1 :(得分:2)

在广泛使用不变性的语言中,有机会在复制时使用“快捷方式”。副本和原始文件可以共享相同的结构,因为您知道它们不能从您下面更改,因为它们是不可变的。只需要创建更改的部分。

From the Clojure Docs on their structures

  

所有Clojure集合都是不可变且持久的。特别是, Clojure集合通过利用结构共享支持高效创建“修改”版本,并为持久使用提供所有性能约束保证。

(强调我的)

基本上,如果你有一个清单

[0 1 2]

你联合(加)到它

(conj [0 1 2] 3)

这会创建一个添加了3的副本。因为原始和副本共享前3个元素,所以不需要复制整个列表。 “副本”引用了原始结构的公共部分,以及3。

你在哪里划线? UI被证明很难仅使用不可变对象。我几乎放弃了完全避免在这个领域的可变性。关键是将其限制在必要的地方。 99%的函数应该包含不变性,然后有1%的函数处理隐藏在某处的杂乱的比特(比如UI回调代码)。这虽然是一条学问的路线,但难以正确沟通。

如有疑问,请将其变为不可变。

答案 2 :(得分:2)

  

这样做的缺点是,每次要对数据结构进行更改时,都必须将整个数据结构复制到一个新对象中,但需要进行所需的更改。

这不是真的。如果您的数据结构由多个节点组成(与例如指针链接),则只需将路径从已更改的部分复制到根目录。

E.g。考虑这个二叉树:

    |
    a
   / \
  b   c
 / \ / \
d  e f  g

如果您想更改节点c中的值,则只需创建ac的修改后的副本:

    |
    a'
   / \
  b   c'
 / \ / \
d  e f  g

结构的其余部分可以重复使用。未更改的节点在树的不同版本之间共享。由于不变性,这种共享是安全的。