我将在先前的回答中补充。
Clojure 的引用类型(原子、引用、代理)与持久数据结构正交。它们与持久数据结构(例如,预期不可变值将包含在所述引用类型中以强制软件事务内存语义等)结合使用,但它们之间没有直接关系。引用类型代表一个身份,例如,某些可能随时间变化的信息,这些连续值通过某个命名实体相关联。
持久数据结构让您可以维护对原始值的引用,并创建表示对原始值语义“修改”的新值(例如,在映射中关联新的键、在向量中更改索引 3 处的值、将项目连接到集合中等)。由于这通常是通过通常的哈希数组映射_TRIE变体(以及排序集合中的红/黑平衡二叉树或在序列中的小签单元格)实现的,因此效率的提高来自于在派生的“新”值和旧值之间尽可能地共享结构。因此,存在某种形式的路径复制方案,其中只需要复制旧树的一个最小子集,然后创建新结构时可以安全地修改复制的子树;其余的都是引用。
除此之外,这还使您能够以纯函数式风格编写代码,这是 clojure 默认的。我们可以编写复杂且有用的程序,而无需基于突变和副作用。由于我们的基本集合是持久的,因此推理基本操作是微不足道的(普遍如此!):我总是知道 assoc、dissoc、conj、disj 等。将不会修改输入并将返回等效值或不同值(与可变容器中的值/等价性语义模糊形成对比)。此类代码风格中的信息流程被提炼为函数、输入和输出,其中信息只能真正单向流动(因为输入永远不会修改)。
这种方法的其他优点是,您还有保留旧版本廉价(内存高效)副本的能力。这并非是内在属性,与引用类型也没有直接关系,但我们可以使用引用类型(例如原子)来实现某种原始的版本控制。
(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 示例中,与 map 中关联的 v1、v2、v3 和 v4 的引用都是不同值,尽管它们之间存在结构共享。它们都是线程安全的,并且在不受其他任何事物影响的情况下持久(因为它们无法更改)。假设空间不是问题,则使用此类方案允许在可能复杂的应用数据层进行简单的“撤销”。在不维护应用状态并保留版本历史记录的情况下轻松记录一切是微不足道的。重置应用程序状态就像从历史记录中弹出项目一样简单。我们不需要进行 delta diff 或生成补丁,因为共享结构已经为我们处理了。
如果您坚持使用持久数据结构来建模应用程序数据,那么您实际上能够以所需的任何粒度对整个“世界”进行快照。