New feature: Atom - shared, synchronous, independent state without locks
One aspect of using immutable data-types like Map
, Seq
, HashSet
, etc. is that in most applications, at some point, you're likely to have some shared reference to one and need to mutate that shared reference. This often requires using synchronisation primitives like lock
(which are not composable and are prone to error).
Atom
With a nod to the atom
type in Clojure language-ext now has two new types:
Atom<A>
Atom<M, A>
These types all wrap a value of A
and provides a method: Swap
(and Prelude function swap
) for atomically mutating the wrapped value without locking.
var atom = Atom(Set("A", "B", "C"));
atom.Swap(old => old.Add("D"));
atom.Swap(old => old.Add("E"));
atom.Swap(old => old.Add("F"));
Debug.Assert(atom == Set("A", "B", "C", "D", "E", "F"));
Atomic update
One thing that must be noted is that if another thread mutates the atom whilst you're running Swap
then your update will rollback. Swap
will then re-run the provided lambda function with the newly updated value (from the other thread) so that it can get a valid updated value to apply. This means that you must be careful to not have side-effects in the Swap
function, or at the very least it needs to be reliably repeatable.
Validation
The Atom
and AtomRef
constructors can take a Func<A, bool>
validation lambda. This is run against the initial value and all subsequent values before being swapped. If the validation lambda returns false
for the proposed value then false
is returned from Swap
and no update takes place.
Events
The Atom
and AtomRef
types all have a Change
event:
public event AtomChangedEvent<A> Change;
It is fired after each successful atomic update to the wrapped value. If you're using the LanguageExt.Rx
extensions then you can also consume the atom.OnChange()
observable.
Metadata
The two types, with an M
generic argument, take an additional meta-data argument on construction which can be used to pass through an environment or some sort of context for the Swap
functions:
var env = new Env();
var atom = Atom(env, Set("A", "B", "C"));
atom.Swap((nenv, old) => old.Add("D"));
Additional arguments
There are also other variants of Swap
that can take up to two additional arguments and pass them through to the lambda:
var atom = Atom(Set(1, 2, 3));
atom.Swap(4, 5, (x, y, old) => old.Add(x).Add(y));
Debug.Assert(atom == Set(1, 2, 3, 4, 5));
Accessing the value
The wrapped value can be accessed by calling atom.Value
or using the implicit operator
conversion to A
.
Pool
The updates to the object pooling system also improves the performance of the Lst
, Set
, and Map
enumerators.
Conclusion
I hope the Atom
types finds some use, I know I've bumped up against this issue many times in the past and have either ended up manually building synchronisation primitives or fallen back to using the ugly ConcurrentDictionary
or similar. I will perhaps take a look at the ref
system in Clojure too - which is a mechanism for atomic updates of multiple items (STM essentially).