我将添加到早期回答中。
Clojure的引用类型(原子、ref、agents)与持久数据结构是正交的。它们通常与持久数据结构(例如,期望不可变值包含在这些引用类型中以强制软件事务内存语义等)一起使用,但它们之间并没有直接关系。引用类型表示一个身份,例如,可能随时间变化的信息,其中连续的值通过某种命名的事物相互关联。
持久数据结构允许您保留对原始值的引用,并创建表示原始(例如,在映射中关联新键,改变向量中的索引3处的值,向集合中conj项目等)语义“修改”的新值。由于这通常是通过通常的哈希数组映射 trie 变体(在排序集合的情况下,红黑平衡二叉树,或者在序列的情况下 cons 单元)完成的,因此效率的提高来自于尽可能多地共享衍生“新”值和旧值之间的结构。因此,存在某种形式的路径复制方案,其中只需要复制旧树的最小子集,然后在创建新结构时安全地修改复制的子树;其他一切都是引用。
除了性能(tries中的最小路径复制使得向量和平铺效率惊人)之外,这还使您能够以纯函数式风格编写代码,这是Clojure默认的。我们可以编写复杂、有用的程序,而不必以基于突变和副作用为基础。由于我们的基本集合是持久的,那么关于基本操作的推理是微不足道的(普遍是这样的!):我始终知道assoc、dissoc、conj、disj等不会修改输入并将返回一个等同于值或不同的值(与可变容器相比,其中值/等价语义是模棱两可的)。以这种风格编写的程序中的信息流被提炼为函数、输入和输出,其中信息只能单向流动(因为输入从不修改)。
这一点的另一个优点是您能够保留旧的版本的便宜(内存高效)副本。这并不是固有的,与引用类型直接无关,但我们可以使用引用类型(如atom)来实现一些简单的版本控制。
(def versions (atom [[]]))
(defn current
([v] (peek @v))
([] (current versions)))
(defn update! [f & args]
(let [newv (apply f (current) args)]
(swap! versions conj newv)
newv))
(defn roll-back!
([v] (-> v (swap! #(if (seq (peek %)) (pop %) %)) peek))
([] (roll-back! versions)))
;; user=> (update! conj 2)
;; [2]
;; user=> (update! conj 3)
;; [2 3]
;; user=> (roll-back!)
;; [2]
;; user=> (roll-back!)
;; []
(let [original (current)
v1 (update! conj 2)
v2 (update! conj 3)
v3 (update! conj 4)
v4 (do (roll-back!) (roll-back!))]
{:v1 v1
:v2 v2
:v3 v3
:v4 v4})
;;{:v1 [2], :v2 [2 3], :v3 [2 3 4], :v4 [2]}
在let演示中,与v1、v2、v3和v4关联的引用在它们之间具有结构性共享,但它们都是不同的值。它们都是线程安全的,并且可以在不影响其他任何事情的情况下持久存在(因为它们不能改变)。假设空间不是问题,使用这种方案可以轻松地在可能复杂的应用程序数据级别上进行“撤消”。维护应用程序状态中的所有内容,并保留以前的版本记录是微不足道的。回滚应用程序状态就像从历史记录中弹出项目一样简单。我们不需要增量差异或生成补丁,因为共享结构已经由我们处理。
如果您继续使用持久数据结构来表示应用程序数据,那么您实际上就拥有了以您想要的任何粒度对整个“世界”进行快照的能力(如果这有意义的话)。