language-ext v3.2.6 Release Notes

Release Date: 2019-07-14 // almost 5 years ago
  • 🔒 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).

    Atom source code