Share your thoughts in the 2024 State of Clojure Survey!

Welcome! Please see the About page for a little more info on how this works.

0 votes
in Refs, agents, atoms by
edited by

Based on my impression a PDS like clojure atom/refs/agents stores all new values in a tree like data-structure as they get changed over-time.

Now, my questions are

  1. In clojure, current values of an identity are could be accesses by defer/@ syntax, is it possible to access all the older values of an atom/ref/agent and print them ? If so, how ?
  2. If older values of an identity are NOT accessible, then what happens to them ? They are still in memory right ? If I cannot access them, what is the point of having a tree like structure ?

2 Answers

+2 votes
by

I will add to the earlier answer.

Clojure's reference types (atoms, ref, agents) are orthogonal to persistent data structures. They are used in conjunction with persistent data structures (e.g. the expectation is that immutable values will be contained in said reference types to enforce software transactional memory semantics and the like), but they aren't directly related. Reference types model an identity, e.g. some information that may vary over time where the successive values are related by some named thing.

Persistent data structures let you maintain references to an original value, and create new values that represent semantic "modifications" to the original (e.g. associate a new key in map, change the value at index 3 in a vector, conj an item to a set, etc.). Since this is accomplished via typically hash array mapped trie variants (and in the case of sorted collections, red/black balanced binary trees, or cons cells in the case of seqs), the efficiency gains come from sharing as much structure as possible between the derived "new" value and the old. So some form of path copying scheme exists where only a minimal subset of the old tree is actually necessary to copy and then safely modify the copied subtree when creating the new structure; everything else is referenced.

Aside from performance (minimal path copying in tries makes for surprisingly efficient vectors and maps), this enables writing in the pure functional style that clojure defaults to. We can write complicated, useful programs without basing them on mutation and side effects. Since our fundamental collections are persistent, then reasoning about the basic operations is trivial (pervasively so!): I always know that assoc, dissoc, conj ,disj, etc. will not modify the input and will return either an equivalent value or a different value (as opposed to a mutable container where value/equality semantics are ambiguous). Information flow through a program written in this style is distilled to functions, input, and output where information can only really flow one direction (since inputs are never modified).

The other upside of this is that you have the ability to retain cheap (memory efficient) copies of older versions. This is not intrinsic and has nothing to do with reference types directly, but we can use a reference type (like an atom) to implement some naive form of versioning:

(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]}

In the let demo, the references associated in the map for v1 v2 v3 and v4 are all different values, although there is structural sharing between them. They are all thread safe and can persist without affecting anything else (since they can't change). Assuming space isn't a concern, using this kind of scheme allows trivial "undo" at potentially complex application data levels. It is trivial to maintain everything in an app state, and retain a log of previous versions. Rewinding the application state is as simple as popping items off of the history. We don't need to delta diff or anything, or generate patches, since the shared structure is already taken care of for us.

If you stick with the persistent data structures to model your application data, then you effectively have the ability to snapshot the entire "world" at whatever level of granularity you would like (if it makes senses).

+1 vote
by
  1. No, unless you kept a reference to the older value.

  2. If no one has a reference to an older value, it is garbage collected.

Persistent data structures reuse parts of older values as they are “changed”, so parts of the older value may be retained if they are still used by newer references.

Welcome to Clojure Q&A, where you can ask questions and receive answers from members of the Clojure community.
...