我意识到这可能是一个非常主观的问题,但我正在寻找那些比我在函数式编程方面更有经验的人的见解。
我的理解是,保持一切不可变的主要动力是让事情更容易理解,并阻止错误蔓延到并行任务。这样做的缺点是,每次要对数据结构进行更改时,都必须将整个数据结构复制到一个新对象中,但需要进行所需的更改。据推测,这样做有一些性能成本:虽然对于一个小对象我不会这么做三次,如果你正在处理大型矩阵或张量或类似的大型数据结构,那肯定会变得非常慢?
简而言之:
答案 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
中的值,则只需创建a
和c
的修改后的副本:
|
a'
/ \
b c'
/ \ / \
d e f g
结构的其余部分可以重复使用。未更改的节点在树的不同版本之间共享。由于不变性,这种共享是安全的。