我将补充早些时候的回答。
Clojure的引用类型(原子、ref、代理)与持续数据结构正交。它们与持续数据结构一起使用(例如,预期不可变值将包含在引用类型中,以强制执行软件事务内存语义等),但它们并不直接相关。引用类型表示一个身份,例如随时间可能发生变化的一些相关信息,其连续值通过某个命名的实体相关联。
持久化数据结构允许你维持对原始值的引用,并创建代表对原始值语义“修改”的新值(例如,在映射中关联新的键,更改向量中索引3的值,将项目合接到集合中等)。由于这是通过通常的哈希数组映射 trie 变体(在排序集合的情况下,红黑平衡二叉树,或者在序列的情况下的 cons 单元)实现的,因此效率的提升来自于在派生的新值和旧值之间共享尽可能多的结构。因此存在某种形式的路径复制方案,其中只需复制旧树的最小子集即可,然后在创建新结构时安全地修改复制的子树;其余的都通过引用。
除了性能之外(在 tries 中最小路径复制使得向量和映射异常高效),这还使得我们可以以 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或生成补丁,因为共享的结构已经为我们处理了。
如果你坚持使用持久化数据结构来建模你的应用程序数据,那么你实际上有在所需的任何粒度级别捕获整个“世界”快照的能力。