1    Introduction

1.1    Overview

std.lib.atom provides enhanced atom operations for managing state with:

  • Swapping with return values
  • Nested path updates
  • Batch operations
  • Change tracking
  • Derived atoms

2    Walkthrough

2.1    Nested reads and writes

std.lib.atom extends clojure.core/atom with path-aware access. atom:get, atom:mget, and atom:keys read nested values; atom:put, atom:set, and atom:set-keys write them.

read nested values

^{:refer std.lib.atom/atom:get :added "3.0"}
(atom:get (atom {:a {:b 1 :c 2}})
          [:a :c])
=> 2

^{:refer std.lib.atom/atom:mget :added "3.0"}
(atom:mget (atom {:a {:b 1 :c 2}})
           [[:a :b] [:a :c]])
=> [1 2]

^{:refer std.lib.atom/atom:keys :added "3.0"}
(atom:keys (atom {:a {:b 1 :c 2}})
           [:a])
=> [:b :c]

write nested values

^{:refer std.lib.atom/atom:put :added "3.0"}
(atom:put (atom {:a {:b 1 :c 2}})
          []
          {:a {:d 3}})
=> [{:a {:b 1 :c 2}}
    {:a {:b 1 :c 2 :d 3}}]

^{:refer std.lib.atom/atom:set :added "3.0"}
(atom:set (atom {:a {:b 1 :c 2}})
          [:a :b] 3
          [:a :d] 5)
=> '([[:a :b] 1 3]
     [[:a :d] nil 5])

2.2    Change tracking

Every write returns a change log. atom:set-changed and atom:put-changed turn that log into a concise :changed summary.

summarise what changed

^{:refer std.lib.atom/atom:set-changed :added "3.0"}
(->> (atom:set (atom {:a {:b 1 :c 2}})
               [:a] {:b 3 :d 4})
     (atom:set-changed))
=> [:changed {:a {:b 3 :d 4}}]

^{:refer std.lib.atom/atom:put-changed :added "3.0"}
(->> (atom:put (atom {:a {:b 1 :c 2}})
               []
               {:a {:d 3}})
     (atom:put-changed))
=> [:changed {:a {:d 3}}]

2.3    Batch updates

atom:batch applies many operations in one go: :set, :swap, :put, and :delete. It returns the full change log and updates the atom.

apply several operations at once

^{:refer std.lib.atom/atom:batch :added "3.0"}
(def -atm- (atom {:a {:b 1 :c 2}}))

(atom:batch -atm-
            [[:set  [:a :b] 3]
             [:swap [:a :c] + 10]
             [:put  [:a :d] {:x 8 :y 9}]
             [:delete [:a :d :x]]])
=> '([[:a :b] 1 3]
     [[:a :c] 2 12]
     [[:a :d] nil {:x 8 :y 9}]
     [[:a :d :x] 8 nil])

3    Basic Operations



swap-return! ^

[atm f] [atm f state?]
Added 3.0

returns output and new state of atom

v 3.0
(defn swap-return!
  ([atm f]
   (swap-return! atm f false))
  ([atm f state?]
   (let [output (volatile! nil)
         new (swap! atm (fn [v]
                          (let [[return new] (f v)
                                _ (vreset! output return)]
                            new)))]
     (if state?
       [@output new]
       @output))))
link
(swap-return! (atom {}) (fn [m] [true (assoc m :a 1)]) true) => [true {:a 1}]

update-diff ^

[m path f & args]
Added 4.0

updates a diff in a sub nesting

v 4.0
(defn update-diff
  [m path f & args]
  (let [out (volatile! nil)
        curr (update-in m path (fn [prev]
                                 (let [[diff curr] (apply f prev args)
                                       _ (vreset! out diff)]
                                   curr)))]
    [@out curr]))
link
(update-diff {:a {:b {:c 1}}} [:a] atom-put-fn [:b] {:d 4} ) => [[{:c 1} {:c 1, :d 4}] {:a {:b {:c 1, :d 4}}}]

4    Nested Access



atom:get ^

[state path]
Added 3.0

gets all the nested keys within an atom

v 3.0
(defn atom:get
  ([state path]
   (get-in (deref state) path)))
link
(atom:get (atom {:a {:b 1 :c 2}}) [:a :c]) => 2

atom:keys ^

[state path]
Added 3.0

lists the nested keys of an atom

v 3.0
(defn atom:keys
  ([state path]
   (keys (get-in (deref state) path))))
link
(atom:keys (atom {:a {:b 1 :c 2}}) [:a]) => [:b :c]

atom:mget ^

[state paths]
Added 3.0

gets all the nested keys within an atom

v 3.0
(defn atom:mget
  ([state paths]
   (let [m (deref state)]
     (mapv #(get-in m %) paths))))
link
(atom:mget (atom {:a {:b 1 :c 2}}) [[:a :b] [:a :c]]) => [1 2]

5    Put and Set



atom:put ^

[state path m]
Added 3.0

puts an entry into the atom

v 3.0
(defn atom:put
  ([state path m]
   (swap-return! state
     (fn [prev] (atom-put-fn prev path m)))))
link
(atom:put (atom {:a {:b 1 :c 2}}) [] {:a {:d 3}}) => [{:a {:b 1, :c 2}} {:a {:b 1, :c 2, :d 3}}]

atom:set ^

[state path m & more]
Added 3.0

sets the entries given a set of inputs

v 3.0
(defn atom:set
  ([state path m & more]
   (let [paths (cons [path m] (partition 2 more))]
     (swap-return! state
       (fn [prev] (atom-set-fn prev paths))))))
link
(atom:set (atom {:a {:b 1 :c 2}}) [:a :b] 3 [:a :d] 5) => '([[:a :b] 1 3] [[:a :d] nil 5]) (-> (atom:set (atom {:a {:b 1 :c 2}}) [:a :b] 3 [:a :d] 5) (atom:set-changed)) => [:changed {:a {:b 3, :d 5}}]

atom:set-keys ^

[state path m]
Added 3.0

sets the entries given a set of inputs

v 3.0
(defn atom:set-keys
  ([state path m]
   (swap-return! state
     (fn [prev] (atom-set-keys-fn prev path m)))))
link
(-> (atom:set-keys (atom {:a {:d 1 :b 1 :c {:a 1}}}) [:a] {:b 4 :c {:b 2}}) (atom:set-changed)) => [:changed {:a {:b 4, :c {:b 2}}}]

6    Change Tracking



atom:put-changed ^

[[old new]]
Added 3.0

figure out what has changed in put operation

v 3.0
(defn atom:put-changed
  ([[old new]]
   (let [changed (c/diff:changed new old)]
     (if (empty? changed)
       [:no-change]
       [:changed changed]))))
link
(->> (atom:put (atom {:a {:b 1 :c 2}}) [] {:a {:d 3}}) (atom:put-changed)) => [:changed {:a {:d 3}}]

atom:set-changed ^

[outputs]
Added 3.0

figure out what has changed in set

v 3.0
(defn atom:set-changed
  ([outputs]
   (let [changed (->> (mapv (fn [[path old new]]
                              [path (if (not= old new)
                                        [:+ new])])
                            outputs)
                      (filter (comp not-empty second))
                      (reduce (fn [out [path changed]]
                                (if (map? changed)
                                  (update-in out path c/merge-nested changed)
                                  (assoc-in out path (second changed))))
                              {}))]
     (if (empty? changed)
       [:no-change]
       [:changed changed]))))
link
(->> (atom:set (atom {:a {:b 1 :c 2}}) [:a] {:b 3 :d 4}) (atom:set-changed)) => [:changed {:a {:b 3, :d 4}}]

7    Batch Operations



atom:batch ^

[state cmds]
Added 3.0

performs a batched operation given keys

v 3.0
(defn atom:batch
  ([state cmds]
   (swap-return! state
     (fn [prev] (atom-batch-fn prev cmds)))))
link
(def -atm- (atom {:a {:b 1 :c 2}})) (atom:batch -atm- [[:set [:a :b] 3] [:swap [:a :c] + 10] [:put [:a :d] {:x 8 :y 9}] [:delete [:a :d :x]]]) => '([[:a :b] 1 3] [[:a :c] 2 12] [[:a :d] nil {:x 8, :y 9}] [[:a :d :x] 8 nil]) @-atm- => {:a {:b 3 :c 12 :d {:y 9}}}

atom:clear ^

[state path]
Added 3.0

clears the previous entry

v 3.0
(defn atom:clear
  ([state path]
   (swap-return! state
                 (fn [prev]
                   (if (empty? path)
                     [prev {}]
                     [(get-in prev path)
                      (c/dissoc-nested prev path)])))))
link
(atom:clear (atom {:a {:b 1 :c 2}}) [:a :b]) => 1

atom:delete ^

[state path & more]
Added 3.0

deletes individual enties from path

v 3.0
(defn atom:delete
  ([state path & more]
   (let [paths (cons path more)]
     (swap-return! state
       (fn [prev] (atom-delete-fn prev paths))))))
link
(atom:delete (atom {:a {:b 1 :c 2}}) [:a :b] [:a :c]) => '([[:a :b] 1] [[:a :c] 2])

8    Derived Atoms



atom:cursor ^

[ref selector] [ref selector key]
Added 3.0

adds a cursor to the atom to swap on any change

v 3.0
(defn atom:cursor
  ([ref selector]
   (atom:cursor ref selector (str (h/sid))))
  ([ref selector key]
   (let [getter  (fn [m] (get-in m selector))
         setter  (fn [m v] (assoc-in m selector v))
         initial (getter @ref)
         cursor  (atom initial)]
     (add-watch ref key (fn [_ _ _ v]
                          (let [cv (getter v)]
                            (if (not= cv @cursor)
                              (reset! cursor cv)))))
     (add-watch cursor key (fn [_ _ _ v]
                             (swap! ref setter v)))
     cursor)))
link
(let [a (atom {:a {:b 1}}) ca (atom:cursor a [:a :b])] (do (swap! ca + 10) (swap! a update-in [:a :b] + 100) [(deref a) (deref ca)])) => [{:a {:b 111}} 111]

atom:derived ^

[atoms f] [atoms f key]
Added 3.0

constructs an atom derived from other atoms

v 3.0
(defn atom:derived
  ([atoms f]
   (atom:derived atoms f (str (h/sid))))
  ([atoms f key]
   (let [cache     (volatile! (map deref atoms))
         derived-fn #(apply f @cache)
         derived  (atom (derived-fn))]
     (doseq [atom atoms]
       (add-watch atom key
                  (fn [_ _ _ _]
                    (let [results (map deref atoms)]
                      (when (not= @cache results)
                        (vreset! cache results)
                        (reset! derived (derived-fn)))))))
     derived)))
link
(let [a (atom 1) b (atom 10) c (atom:derived [a b] +)] (do (swap! a + 1) (swap! b + 10) [@a @b @c])) => [2 20 22]

9    std.lib.atom: A Comprehensive Summary

The std.lib.atom namespace provides a set of extended utilities for working with Clojure's atom data structure, focusing on nested updates, batch operations, and derived/cursor-like functionalities. It aims to simplify common patterns of managing mutable state, especially when dealing with deeply nested maps or when needing to track changes.

9.1    Core Concepts:

  • Nested State Management: Many functions in this namespace are designed to operate on nested data within an atom, using paths (vectors of keys) to specify target locations.n Transactional Updates: Operations like swap-return! and atom:batch ensure that updates to the atom are atomic and provide mechanisms to inspect the changes or the new state.n Change Tracking: Functions like atom:put-changed and atom:set-changed help identify what parts of the atom's state have actually been modified after an operation.n Derived State: atom:derived allows creating atoms whose values are computed from other atoms, automatically updating when their dependencies change.n Cursors: atom:cursor provides a way to focus on a specific part of a larger atom, allowing updates to that sub-part to be reflected in the parent atom and vice-versa.

9.2    Key Functions and Macros:

  • update-diff:n Purpose: Updates a value at a specific path within a map, returning the difference ([old-value new-value]) and the new map.n Usage: (update-diff {:a {:b 1}} [:a] my-update-fn :c 2)n swap-return!:n Purpose: Similar to clojure.core/swap!, but returns the result of the update function (the "output") and optionally the new state of the atom.n Usage: (swap-return! my-atom (fn [v] [(:some-key v) (update v :count inc)]))n atom:keys:n Purpose: Returns the keys of a map at a specified nested path within the atom's dereferenced value.n Usage: (atom:keys my-atom [:path :to :map])n atom:get:n Purpose: Retrieves the value at a specified nested path within the atom's dereferenced value.n Usage: (atom:get my-atom [:path :to :value])n atom:mget:n Purpose: Retrieves multiple values from different nested paths within the atom.n Usage: (atom:mget my-atom [[:path1] [:path2]])n atom:put:n Purpose: Merges a map m into the value at path within the atom. If path is empty, m is merged with the root.n Usage: (atom:put my-atom [:user :profile] {:name "Alice"})n atom:set:n Purpose: Sets specific values at multiple nested paths within the atom. It takes path-value pairs.n Usage: (atom:set my-atom [:a :b] 10 [:x :y] 20)n atom:set-keys:n Purpose: Sets multiple keys within a map at a given path.n Usage: (atom:set-keys my-atom [:user] {:name "Bob" :age 30})n atom:set-changed:n Purpose: Analyzes the output of atom:set (a list of [path old-value new-value] tuples) and returns a map representing only the changed values.n Usage: (atom:set-changed outputs)n atom:put-changed:n Purpose: Analyzes the output of atom:put ([old-state new-state]) and returns a map representing only the changed values.n Usage: (atom:put-changed [old-map new-map])n atom:swap:n Purpose: Applies functions to values at multiple nested paths within the atom. It takes path-function pairs.n Usage: (atom:swap my-atom [:a :count] inc [:b :total] + 10)n atom:delete:n Purpose: Deletes values at multiple specified nested paths within the atom.n Usage: (atom:delete my-atom [:user :email] [:user :address])n atom:clear:n Purpose: Clears (sets to {}) the value at a specified path, or clears the entire atom if the path is empty.n Usage: (atom:clear my-atom [:temp :data])n atom:batch:n Purpose: Performs a sequence of set, put, swap, or delete operations on the atom in a single atomic transaction.n Usage: (atom:batch my-atom [[:set [:a] 1] [:swap [:b] inc]])n atom:cursor:n Purpose: Creates a new atom (the "cursor") that reflects a sub-section of a parent atom. Changes to the cursor atom are propagated back to the parent, and changes in the parent are reflected in the cursor.n Usage: (atom:cursor parent-atom [:path :to :subatom])n atom:derived:n Purpose: Creates a new atom whose value is derived by applying a function f to the dereferenced values of a collection of other atoms. The derived atom automatically updates when any of its source atoms change.n * Usage: (atom:derived [atom1 atom2] +)

9.3    Usage Pattern:

This namespace is particularly useful for managing complex application state where:

  • State is often represented as deeply nested maps.n Multiple parts of the state need to be updated atomically.n Changes need to be tracked or reacted to.n* Specific sub-sections of the state need to be exposed as independent, yet synchronized, atoms.

By providing these higher-level abstractions, std.lib.atom simplifies common state management challenges in Clojure applications.