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
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}]
4 Nested Access
5 Put and Set
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}}]
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}}]
6 Change Tracking
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}}]
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
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}}}
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
8 Derived Atoms
atom:cursor ^
[ref selector] [ref selector key]
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]
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!andatom:batchensure that updates to the atom are atomic and provide mechanisms to inspect the changes or the new state.n Change Tracking: Functions likeatom:put-changedandatom:set-changedhelp identify what parts of the atom's state have actually been modified after an operation.n Derived State:atom:derivedallows creating atoms whose values are computed from other atoms, automatically updating when their dependencies change.n Cursors:atom:cursorprovides 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)nswap-return!:n Purpose: Similar toclojure.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)]))natom: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])natom: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])natom:mget:n Purpose: Retrieves multiple values from different nested paths within the atom.n Usage:(atom:mget my-atom [[:path1] [:path2]])natom:put:n Purpose: Merges a mapminto the value atpathwithin the atom. Ifpathis empty,mis merged with the root.n Usage:(atom:put my-atom [:user :profile] {:name "Alice"})natom: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)natom: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})natom:set-changed:n Purpose: Analyzes the output ofatom: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)natom:put-changed:n Purpose: Analyzes the output ofatom:put([old-state new-state]) and returns a map representing only the changed values.n Usage:(atom:put-changed [old-map new-map])natom: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)natom:delete:n Purpose: Deletes values at multiple specified nested paths within the atom.n Usage:(atom:delete my-atom [:user :email] [:user :address])natom: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])natom:batch:n Purpose: Performs a sequence ofset,put,swap, ordeleteoperations on the atom in a single atomic transaction.n Usage:(atom:batch my-atom [[:set [:a] 1] [:swap [:b] inc]])natom: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])natom:derived:n Purpose: Creates a new atom whose value is derived by applying a functionfto 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.