Clojure-style Atoms
To use the bindings from this module:
(import :std/misc/atom)
Atoms provide a way to manage shared, synchronous, independent state, closely following the API defined by Clojure.
Compared to the Clojure atom API, we:
- prefix all function names with
atom-
; - do not support the meta field and the functions
alter-meta!
orreset-meta!
; - replace Clojure
swap-vals!
andreset-vals!
by ourswap-values!
andreset-values!
that return values rather than vectors; - rename
add-watch
andremove-watch
toatom-add-watch!
andatom-remove-watch!
with theatom-
prefix and the!
suffix; - do not have generic functions that work on multiple kinds of "references" but only one set of functions for atoms;
- also export
atom-increment!
andatomic-counter
for the common case.
You create an atom with atom
, and can access its state with atom-deref
.
Just like Clojure atoms and other references, our atoms support validators:
functions that validate new values.
To change the value of an atom, you can use atom-swap!
or its variants
atom-swap-values!
, atom-reset!
, atom-reset-values!
,
and for number-valued atoms, atom-increment!
,
that will atomically change the value to which the atom is set,
by applying a transformation function to the old value
and checking that the atom’s validator accepts the new value.
A lower-level compare-and-set!
is also provided.
Changes to atoms are always applied in a sequence of disjoint computations,
free of race conditions.
Unlike the Clojure implementation, our current implementation does not retry, though we do not guarantee not to change in the future to match Clojure. Unlike in Clojure, it is thus acceptable for the change function to have side-effects, though it is not recommended, in case this behavior changes.
atom
(atom (initial-value (void)) validator: (validator #f)) -> Atom
Creates a new atom, a box that atomically changes value.
If no initial-value
is provided, (void)
is used (Clojure has no default initial-value).
A validator:
keyword argument can specify a validator
(defaults to #f
which designates a validator that always says "yes").
The validator takes a new value and returns true if it is accepted.
If the validator returns #f
or throws, the value is rejected and the atom is left unmodified;
furthermore, after releasing the lock and allowing other threads to modify the atom,
an exception is raised for the caller of the modifying function:
either the exception thrown by the validator, if any,
or a BadArgument
error raised if the validator returned #f
.
Similar to Clojure’s atom
.
Examples:
> (import :std/iter :std/misc/atom)
> (def a (atom 0))
> (def workers (for/collect (i (in-range 100)) (spawn atom-swap! a 1+)))
> (begin (for-each thread-join! workers) (atom-deref a))
100
atom?
(atom? a) -> boolean
Returns #t
if a
an atom, #f
otherwise.
Examples:
> (import :std/misc/atom)
> (atom? (atom))
#t
> (atom 42)
#<Atom #8>
> (atom? #8)
#t
atom-swap!
(atom-swap! atom function) -> new-value
Applies the function
to the value currently in the atom
, and,
if it passes the atom
’s validator (see the atom
function above),
sets the atom
to the new value.
Ensures that no other access is made to the atom
in the middle of a swap.
Returns the new value.
Examples:
> (def a (atom 3))
> (atom-swap! a (cut * 2 <>))
6
atom-swap-values!
(atom-swap! atom function) -> old-value new-value
Similar to atom-swap!
but returns two values, the old and the new values.
Similar to Clojure’s swap-vals!
but returns two values rather than a vector of size 2.
Examples:
> (import :std/misc/atom)
> (def a (atom 6))
> (atom-swap-values! a (cut * 2 <>))
6
12
atom-reset!
(atom-reset! atom new-value) -> new-value
Sets the atom to the provided new value,
if it passes the atom
’s validator (see the atom
function above),
Ensures that no other access is made to the atom in the middle of a reset.
Returns the new value.
Examples:
> (import :std/misc/atom)
> (def a (atom 42))
> (atom-reset! a 23)
23
> (atom-deref a)
23
atom-reset-values!
(atom-reset-values! atom new-value) -> old-value new-value
Similar to atom-reset!
but returns two values, the old and the new.
Similar to Clojure atom-reset-vals!
but returns values instead of a vector.
Examples:
> (import :std/misc/atom)
> (def a (atom 42))
> (atom-reset-values! a 23)
42
23
> (atom-deref a)
23
atom-compare-and-set!
(atom-compare-and-set! atom oldval newval) -> success?
Atomically sets the value of atom
to newval
if and only if the
current value of the atom is identical to oldval
,
and the new value passes the atom
’s validator (see the atom
function above).
Returns #t
if set happened, else #f
(but still throws an exception in case of validation error).
Similar to Clojure compare-and-set!
.
Examples:
> (import :std/misc/atom)
> (def a (atom 42))
> (atom-compare-and-set! a 42 23)
#t
> (atom-deref a)
23
> (atom-compare-and-set! a 42 24)
#f
> (atom-deref a)
23
atom-validator
(atom-validator atom) -> validator
Access the current atom’s validator, a function that takes a new value as argument
and returns a true value iff the atom will accept that value.
The value #f
is a stand-in for not doing any validation,
which is equivalent to the function true
that accepts every value.
Similar to Clojure get-validator
.
Examples:
> (import :std/misc/atom)
> (def a (atom 42 validator: number?))
> (atom-reset! a 1)
1
> (atom-reset! a "foo")
*** ERROR IN with-lock --
*** ERROR IN "misc/atom.ss"@48.29 [BadArgument]: bad argument; expected valid atom value
--- irritants: update-atom! "foo"
atom-validator-set!
(atom-validator-set! atom validator) -> _
Modify the current atom
’s validator
, a function that takes a new value as argument
and returns a true value iff the atom
will accept that value.
The value #f
is a stand-in for not doing any validation,
which is behaviorally equivalent to the function true
that accepts every value.
The current value is grandfathered and not validated;
the validator
is not validated for whether it accepts the current value.
The function is not guaranteed to return any specific value.
Similar to Clojure set-validator!
.
Examples:
> (import :std/misc/atom)
> (def a (atom 42 validator: number?))
> (atom-reset! a 1)
1
> (atom-validator-set! a string?)
> (atom-reset! a "foo")
"foo"
> (atom-reset! a 2)
*** ERROR IN "misc/atom.ss"@48.29 [BadArgument]: bad argument; expected valid atom value
--- irritants: update-atom! 1
atom-add-watch!
(atom-add-watch! atom key watch-function) -> void
Adds a watch-function
to an atom
, identified by given key
.
The watch-function
must be a function of 4 args:
the key
, the atom
, its old-state
, its new-state
.
Whenever the atom
’s state is changed,
after the new value has been validated,
each of the registered watches will have their functions called,
in an unspecified order, while the atom
lock is still held
(so no changes to the atom
are allowed in the watches themselves,
though they may send asynchronous messages or spawn threads that
will cause a cascade of further changes).
Keys must be unique per atom, and can be used to remove the watch with
atom-remove-watch!
, but are otherwise considered opaque
by the watch mechanism.
Similar to Clojure’s add-watch
function.
It is unspecified whether the state has been modified yet,
so use old-state
and new-state
rather than atom-deref
.
Note also that watch functions may be called from multiple threads
simultaneously.
In the current implementation, the state will already have been modified
and no threads will be spawned, but there is no guarantee we won’t change that.
Examples:
> (import :std/misc/atom :std/misc/list)
> (def transitions [])
> (def a (atom 1))
> (atom-add-watch! a 'record-transitions
(lambda (_k _a old new) (push! [old new] transitions)))
> (atom-swap! a (cut * 2 <>))
2
> (atom-swap! a (cut * 2 <>))
4
> transitions
((2 4) (1 2))
> (atom-remove-watch! a 'record-transitions)
> (atom-swap! a (cut * 2 <>))
8
> transitions
((2 4) (1 2))
atom-remove-watch!
(atom-remove-watch! atom key) -> void
Removes a watch function to an atom
, the one associated to the given key
if any.
Similar to Clojure’s remove-watch
function.
See example for atom-add-watch!
above.
atom-increment!
(atom-increment! atom [increment]) -> new-numeric-value
Increments the atom
value by the specified increment
, or by 1
if left unspecified.
Return the new value. Common special case for a numeric (often integer) atom.
Examples:
> (import :std/misc/atom)
> (def a (atom -1))
> (atom-increment! a)
0
> (atom-increment! a)
1
> (atom-increment! a)
2
atomic-counter
(atomic-counter [initial-value]) -> function
Creates a function that maintains an atomic-counter
that starts with the given initial-value
(which defaults to -1
),
and increments the atomic value by 1
, returning the new value, each time it is called.
Note that for performance reasons, you cannot specify a different increment;
if this is what you need, make your own abstraction based on atom
(and if you use it a lot... maybe contribute it to this library, or another).
Examples:
> (import :std/misc/atom)
> (def my-counter (atomic-counter))
> (my-counter)
0
> (my-counter)
1
> (my-counter)
2
> (my-counter 10)
12
> (my-counter 0)
12
> (def other-counter (atomic-counter 42))
> (other-counter)
43
> (other-counter)
44