Skip to content
AgileLogics
6 min read

Atoms

Prerequisites: Higher-order functions

Values never change, but programs have state that does: a counter, a cache, the current users. Clojure models state as a reference that points to an immutable value and can be switched, safely, to point to a new one. The simplest reference type is the atom.

(def counter (atom 0))

(println @counter)                  ; deref: read the current value
(swap! counter inc)                 ; apply a function to it
(swap! counter + 10)
(println @counter)
(reset! counter 100)                ; set a new value directly
(println (deref counter))
0
11
100
  • @atom (or (deref atom)) reads the current value.
  • (swap! atom f args...) sets the value to (f current args...) and returns the new value.
  • reset! replaces the value without looking at the old one.

Atoms holding maps

(def app-state (atom {:users {} :next-id 1}))

(defn add-user! [name]
  (swap! app-state
         (fn [{:keys [next-id] :as state}]
           (-> state
               (assoc-in [:users next-id] {:id next-id :name name})
               (update :next-id inc)))))

(add-user! "Ada")
(add-user! "Linus")
(println (:users @app-state))
(println (:next-id @app-state))
{1 {:id 1, :name Ada}, 2 {:id 2, :name Linus}}
3

The typical Clojure application keeps a few atoms of plain data and updates them with pure functions. The ! suffix marks functions that change state.

Thread safety

(def hits (atom 0))

(let [workers (doall (for [_ (range 8)]
                       (future (dotimes [_ 1000] (swap! hits inc)))))]
  (run! deref workers))

(println @hits)
(shutdown-agents)
8000

Eight threads (created with future — see Futures, Promises, and Delays) incremented the atom 8,000 times without a lock, and no update was lost. swap! works by compare-and-set: if another thread changed the atom in the meantime, it retries with the new value. That's why the function you pass to swap! must be pure — it may run more than once.

Watches and validators

(def temperature (atom 20 :validator #(<= -50 % 60)))

(add-watch temperature :logger
           (fn [_key _ref old new]
             (println "temperature" old "->" new)))

(swap! temperature + 5)
(reset! temperature 18)
(try
  (reset! temperature 500)
  (catch IllegalStateException e
    (println "rejected:" (ex-message e))))
(println @temperature)
temperature 20 -> 25
temperature 25 -> 18
rejected: Invalid reference state
18
  • A validator rejects invalid values (the atom keeps the old one).
  • A watch runs after every change — useful for logging, UI updates, or keeping a derived value in sync.

Practice

Build a tiny shopping cart with an atom holding a map of item to quantity, and functions add-item! and remove-item!.

Show solution
(def cart (atom {}))

(defn add-item! [item]
  (swap! cart update item (fnil inc 0)))

(defn remove-item! [item]
  (swap! cart (fn [c]
                (if (> (get c item 0) 1)
                  (update c item dec)
                  (dissoc c item)))))

(add-item! :apple)
(add-item! :apple)
(add-item! :bread)
(remove-item! :bread)
(remove-item! :apple)
(println @cart)
{:apple 1}