All Versions
37
Latest Version
Avg Release Cycle
20 days
Latest Release
1290 days ago

Changelog History
Page 3

  • v3.3.11 Changes

    August 15, 2019

    ๐Ÿš€ This is an important fix of an issue with the new HashMap and HashSet implementation (to patch any release since v3.3.0). There was a very sporadic issue with reading items from the map after removal of other items, which would seemingly create random/undefined behaviour, and so it is advised that you upgrade to this release asap.

  • v3.3.10 Changes

    August 14, 2019

    ๐Ÿš€ Following on from the last two releases:

    The LanguageExt.CodeGen system now supports wrapping the Reader/Writer/State (RWS) monad.

    Usage of the RWS monad is complicated because it takes so many generic arguments:

    RWS\<MonoidW, R, W, S, A\> where MonoidW : struct, Monoid\<W\>
    

    And so you can be forgiven for not giving it a try. For, the unitiated, the RWS monad is a super-powered monad that combines the features of the:

    • Reader monad - in that is can take an environment. Think of this as read-only configuration state. This is useful to keep functions pure when working with 'global' state.
    • ๐ŸŒฒ Writer monad - which maintains a state that is a monoid. The most common monoid to use here would be a Seq<W>. The writer monad allows for easy logging of items without needing access to an external log. Again, keeping the computation pure. But can also be used to sum totals, concatenate strings, etc.
    • โšก๏ธ State monad - which is like the Reader monad in that it manages a state object, but the state can be updated as the computation runs.

    And so all of these features in a single monad provides a very powerful set of tools for writing pure functions. The unfortunate aspect of this is that the majority of the generic parameters are fixed for the lifetime of the computation but need to be provided far too regularly due to C#'s poor type-inference story: MonoidW, R, W. And even S could be fixed depending on its use-case.

    ๐Ÿš€ And so this is where this release comes in, a code-gen system that wraps the RWS monad into something a bit more manageable.

     [RWS(WriterMonoid: typeof(MSeq\<string\>), Env: typeof(IO))] public partial struct Subsys\<S, T\> {}
    

    โšก๏ธ The code above will create a wrapped RWS monad with an S state that can vary (by calling put(x)). Below, is a version that doesn't have a variable S generic parameter, and instead has a fixed state embedded in the wrapped RWS monad. So, put can only be called with another Person to update the state.

     [RWS(WriterMonoid: typeof(MSeq\<string\>), Env: typeof(IO), State: typeof(Person))] public partial struct Subsys\<T\> { }
    

    The generated code for the second option looks like this:

    public partial struct Subsys\<T\> { readonly internal LanguageExt.RWS\<LanguageExt.ClassInstances.MSeq\<string\>, TestBed.IO, LanguageExt.Seq\<string\>, TestBed.Person, T\> \_\_comp; internal Subsys(LanguageExt.RWS\<LanguageExt.ClassInstances.MSeq\<string\>, TestBed.IO, LanguageExt.Seq\<string\>, TestBed.Person, T\> comp) =\> \_\_comp = comp; public static Subsys\<T\> Pure(T value) =\> new Subsys\<T\>((env, state) =\> (value, default, state, false)); public static Subsys\<T\> Fail =\> new Subsys\<T\>((env, state) =\> (default, default, default, true)); public Subsys\<U\> Map\<U\>(Func\<T, U\> f) =\> new Subsys\<U\>(\_\_comp.Map(f)); public Subsys\<U\> Select\<U\>(Func\<T, U\> f) =\> new Subsys\<U\>(\_\_comp.Map(f)); public Subsys\<U\> Bind\<U\>(Func\<T, Subsys\<U\>\> f) =\> new Subsys\<U\>(\_\_comp.Bind(a =\> f(a).\_\_comp)); public Subsys\<U\> SelectMany\<U\>(Func\<T, Subsys\<U\>\> f) =\> new Subsys\<U\>(\_\_comp.Bind(a =\> f(a).\_\_comp)); public Subsys\<V\> SelectMany\<U, V\>(Func\<T, Subsys\<U\>\> bind, Func\<T, U, V\> project) =\> new Subsys\<V\>(\_\_comp.Bind(a =\> bind(a).\_\_comp.Map(b =\> project(a, b)))); public (TryOption\<T\> Value, LanguageExt.Seq\<string\> Output, TestBed.Person State) Run(TestBed.IO env, TestBed.Person state) =\> \_\_comp.Run(env, state); public Subsys\<T\> Filter(Func\<T, bool\> f) =\> new Subsys\<T\>(\_\_comp.Where(f)); public Subsys\<T\> Where(Func\<T, bool\> f) =\> new Subsys\<T\>(\_\_comp.Where(f)); public Subsys\<T\> Do(Action\<T\> f) =\> new Subsys\<T\>(\_\_comp.Do(f)); public Subsys\<T\> Strict() =\> new Subsys\<T\>(\_\_comp.Strict()); public Seq\<T\> ToSeq(TestBed.IO env, TestBed.Person state) =\> \_\_comp.ToSeq(env, state); public Subsys\<LanguageExt.Unit\> Iter(Action\<T\> f) =\> new Subsys\<LanguageExt.Unit\>(\_\_comp.Iter(f)); public Func\<TestBed.IO, TestBed.Person, State\> Fold\<State\>(State state, Func\<State, T, State\> f) { var self = this; return (env, s) =\> self.\_\_comp.Fold(state, f).Run(env, s).Value.IfNoneOrFail(state); } public Func\<TestBed.IO, TestBed.Person, bool\> ForAll(Func\<T, bool\> f) { var self = this; return (env, s) =\> self.\_\_comp.ForAll(f).Run(env, s).Value.IfNoneOrFail(false); } public Func\<TestBed.IO, TestBed.Person, bool\> Exists(Func\<T, bool\> f) { var self = this; return (env, s) =\> self.\_\_comp.Exists(f).Run(env, s).Value.IfNoneOrFail(false); } public Subsys\<T\> Local(Func\<TestBed.IO, TestBed.IO\> f) =\> new Subsys\<T\>(LanguageExt.Prelude.local\<LanguageExt.ClassInstances.MSeq\<string\>, TestBed.IO, LanguageExt.Seq\<string\>, TestBed.Person, T\>(\_\_comp, f)); public Subsys\<(T, U)\> Listen\<U\>(Func\<LanguageExt.Seq\<string\>, U\> f) =\> new Subsys\<(T, U)\>(\_\_comp.Listen(f)); public Subsys\<T\> Censor(Func\<LanguageExt.Seq\<string\>, LanguageExt.Seq\<string\>\> f) =\> new Subsys\<T\>(\_\_comp.Censor(f)); } public static partial class Subsys { public static Subsys\<T\> Pure\<T\>(T value) =\> Subsys\<T\>.Pure(value); public static Subsys\<T\> Fail\<T\>() =\> Subsys\<T\>.Fail; public static Subsys\<T\> asks\<T\>(Func\<TestBed.IO, T\> f) =\> new Subsys\<T\>((env, state) =\> (f(env), default, state, false)); public static Subsys\<TestBed.IO\> ask =\> new Subsys\<TestBed.IO\>((env, state) =\> (env, default, state, false)); public static Subsys\<TestBed.Person\> get =\> new Subsys\<TestBed.Person\>((env, state) =\> (state, default, state, false)); public static Subsys\<T\> gets\<T\>(Func\<TestBed.Person, T\> f) =\> new Subsys\<T\>((env, state) =\> (f(state), default, state, false)); public static Subsys\<LanguageExt.Unit\> put(TestBed.Person value) =\> new Subsys\<LanguageExt.Unit\>((env, state) =\> (default, default, value, false)); public static Subsys\<LanguageExt.Unit\> modify\<T\>(Func\<TestBed.Person, TestBed.Person\> f) =\> new Subsys\<LanguageExt.Unit\>((env, state) =\> (default, default, f(state), false)); public static Subsys\<T\> local\<T\>(Subsys\<T\> ma, Func\<TestBed.IO, TestBed.IO\> f) =\> ma.Local(f); public static Subsys\<T\> Pass\<T\>(this Subsys\<(T, Func\<LanguageExt.Seq\<string\>, LanguageExt.Seq\<string\>\>)\> ma) =\> new Subsys\<T\>(ma.\_\_comp.Pass()); public static Subsys\<T\> pass\<T\>(Subsys\<(T, Func\<LanguageExt.Seq\<string\>, LanguageExt.Seq\<string\>\>)\> ma) =\> new Subsys\<T\>(ma.\_\_comp.Pass()); public static Subsys\<(T, U)\> listen\<T, U\>(Subsys\<T\> ma, Func\<LanguageExt.Seq\<string\>, U\> f) =\> ma.Listen(f); public static Subsys\<T\> censor\<T\>(Subsys\<T\> ma, Func\<LanguageExt.Seq\<string\>, LanguageExt.Seq\<string\>\> f) =\> ma.Censor(f); public static Subsys\<LanguageExt.Unit\> tell(LanguageExt.Seq\<string\> what) =\> new Subsys\<LanguageExt.Unit\>(tell\<LanguageExt.ClassInstances.MSeq\<string\>, TestBed.IO, LanguageExt.Seq\<string\>, TestBed.Person, LanguageExt.Unit\>(what)); public static Subsys\<LanguageExt.Seq\<string\>\> ReadAllLines(string fileName) =\> ask.Map(\_\_env =\> \_\_env.ReadAllLines(fileName)); public static Subsys\<LanguageExt.Unit\> WriteAllLines(string fileName, LanguageExt.Seq\<string\> lines) =\> ask.Map(\_\_env =\> \_\_env.WriteAllLines(fileName, lines)); public static Subsys\<int\> Zero =\> ask.Map(\_\_env =\> \_\_env.Zero); public static Subsys\<string\> Name =\> get.Map(\_\_env =\> \_\_env.Name); public static Subsys\<string\> Surname =\> get.Map(\_\_env =\> \_\_env.Surname); }
    

    You'll notice toward the end that the members of IO and Person are available to use directly. So, you can write:

    using Subsys; var fullName = from n in Namefrom s in Surnameselect $"{n} {s}";
    

    It's possible to pick'n'choose which features of the RWS you want to use by putting in Unit or MUnit for the types. This is a State and Reader monad with no useful Writer functionality:

     [RWS(WriterMonoid: typeof(MUnit), Env: typeof(IO), State: typeof(Person))] public partial struct Subsys\<T\> {}
    

    As, with the Reader code-gen, it's possible to modify the constructor and failure functions:

     [RWS(WriterMonoid: typeof(MSeq\<string\>), Env: typeof(IO), State: typeof(Person), Constructor: "Pure", Fail: "Error")] public partial struct Subsys\<T\> {}
    
  • v3.3.8 Changes

    August 12, 2019

    ๐Ÿš€ The last release instroduced the new monad builder feature that wraps a Reader<Env, A> monad into a new monad with the Env hidden inside to make it easier to use.

    ๐Ÿš€ This release improves on that feature by extending the static class that's generated with all functions, fields, and properties that are in the Env type.

    And so, if we have an Env type like so:

    public interface IOEnv { Seq\<string\> ReadAllLines(string path); Unit WriteAllLines(string path, Seq\<string\> lines); }
    

    And wrap it in a new monad called IO:

     [Reader(typeof(IOEnv))] public partial struct IO\<A\> {}
    

    Then we can use the methods within the IOEnv type like so:

    var comp = from ls in IO.ReadAllLines("c:/test.txt") from \_\_ in IO.WriteAllLines("c:/test-copy.txt", ls) select ls.Count;
    

    Which simplifies the usage of the Reader monad even more.

    NOTE: There is no requirement to use an interface for the Env, it can be any type.

    It is also possible to specify the name of the constructor and failure functions: Return and Fail.

     [Reader(Env: typeof(IOEnv), Constructor: "LiftIO", Fail: "FailIO")] public partial struct IO\<A\> { }
    

    This makes it much easier to use the static class as a namespace:

    using static IO; var comp = from ls in ReadAllLines("c:/test.txt") from \_\_ in WriteAllLines("c:/test-copy.txt", ls) from xin LiftIO(100) from yin LiftIO(200) select x \* y;
    

    The generated code for the above example looks like this:

    public partial struct IO\<A\> { readonly LanguageExt.Reader\<TestBed.IOEnv, A\> \_\_comp; internal IO(LanguageExt.Reader\<TestBed.IOEnv, A\> comp) =\> \_\_comp = comp; public static IO\<A\> LiftIO(A value) =\> new IO\<A\>(env =\> (value, false)); public static IO\<A\> FailIO =\> new IO\<A\>(env =\> (default, true)); public IO\<B\> Map\<B\>(Func\<A, B\> f) =\> new IO\<B\>(\_\_comp.Map(f)); public IO\<B\> Select\<B\>(Func\<A, B\> f) =\> new IO\<B\>(\_\_comp.Map(f)); public IO\<B\> Bind\<B\>(Func\<A, IO\<B\>\> f) =\> new IO\<B\>(\_\_comp.Bind(a =\> f(a).\_\_comp)); public IO\<B\> SelectMany\<B\>(Func\<A, IO\<B\>\> f) =\> new IO\<B\>(\_\_comp.Bind(a =\> f(a).\_\_comp)); public IO\<C\> SelectMany\<B, C\>(Func\<A, IO\<B\>\> bind, Func\<A, B, C\> project) =\> new IO\<C\>(\_\_comp.Bind(a =\> bind(a).\_\_comp.Map(b =\> project(a, b)))); public TryOption\<A\> Run(TestBed.IOEnv env) =\> \_\_comp.Run(env); public IO\<A\> Filter(Func\<A, bool\> f) =\> new IO\<A\>(\_\_comp.Where(f)); public IO\<A\> Where(Func\<A, bool\> f) =\> new IO\<A\>(\_\_comp.Where(f)); public IO\<A\> Do(Action\<A\> f) =\> new IO\<A\>(\_\_comp.Do(f)); public IO\<A\> Strict() =\> new IO\<A\>(\_\_comp.Strict()); public Seq\<A\> ToSeq(TestBed.IOEnv env) =\> \_\_comp.ToSeq(env); public IO\<LanguageExt.Unit\> Iter(Action\<A\> f) =\> new IO\<LanguageExt.Unit\>(\_\_comp.Iter(f)); public Func\<TestBed.IOEnv, S\> Fold\<S\>(S state, Func\<S, A, S\> f) { var self = this; return env =\> self.\_\_comp.Fold(state, f).Run(env).IfNoneOrFail(state); } public Func\<TestBed.IOEnv, bool\> ForAll\<S\>(S state, Func\<A, bool\> f) { var self = this; return env =\> self.\_\_comp.ForAll(f).Run(env).IfNoneOrFail(false); } public Func\<TestBed.IOEnv, bool\> Exists\<S\>(S state, Func\<A, bool\> f) { var self = this; return env =\> self.\_\_comp.Exists(f).Run(env).IfNoneOrFail(false); } }public static partial class IO{ public static IO\<A\> LiftIO\<A\>(A value) =\> IO\<A\>.LiftIO(value); public static IO\<A\> FailIO\<A\>() =\> IO\<A\>.FailIO; public static IO\<A\> asks\<A\>(Func\<TestBed.IOEnv, A\> f) =\> new IO\<A\>(env =\> (f(env), false)); public static readonly IO\<TestBed.IOEnv\> ask = new IO\<TestBed.IOEnv\>(env =\> (env, false)); public static IO\<LanguageExt.Seq\<string\>\> ReadAllLines(string path) =\> ask.Map(\_\_env =\> \_\_env.ReadAllLines(path)); public static IO\<LanguageExt.Unit\> WriteAllLines(string path, LanguageExt.Seq\<string\> lines) =\> ask.Map(\_\_env =\> \_\_env.WriteAllLines(path, lines)); }
    
  • v3.3.7 Changes

    August 08, 2019

    ๐Ÿ”ง A common use for the Reader monad is to pass through a static environment. This can often be configuration, but it could also be a collection of functions for doing dependency injection (doing it well, rather than the OO way).

    For example:

    public interface IO { Seq\<string\> ReadAllLines(string path); Unit WriteAllLines(string path, Seq\<string\> lines); } public class RealIO : IO { public Seq\<string\> ReadAllLines(string path) =\> File.ReadAllLines(path).ToSeq(); public Unit WriteAllLines(string path, Seq\<string\> lines) { File.WriteAllLines(path, lines); return unit; } }
    

    This can then be used in a Reader computation:

    var comp = from io in ask\<IO\>() let ls = io.ReadAllLines("c:/test.txt") let \_= io.WriteAllLines("c:/test-copy.txt", ls) select ls.Count;
    

    ๐Ÿคก Then the comp can be run with a real IO environment or a mocked one:

    comp.Run(new RealIO());
    

    However, carrying around the, non-changing, generic environment argument has a cognitive overhead and causes lots of extra typing.

    And so now it's possible to use the LanguageExt.CodeGen to wrap up the Reader<Env, A> into a simpler monad. i.e.

     [Reader(typeof(IO))] public partial struct Subsystem\<A\> { }
    

    NOTE: For now the new monadic type must be a struct

    NOTE ALSO: If you use multiple generic parameters then the last one will be the bound value type

    ๐Ÿ— When providing the [Reader...] attribute with the type of the environment parameter, the code-gen will build:

    public partial struct Subsystem\<A\> { readonly LanguageExt.Reader\<TestBed.IO, A\> \_\_comp; internal Subsystem(LanguageExt.Reader\<TestBed.IO, A\> comp) =\> \_\_comp = comp; public static Subsystem\<A\> Return(A value) =\> new Subsystem\<A\>(env =\> (value, false)); public static Subsystem\<A\> Fail =\> new Subsystem\<A\>(env =\> (default, true)); public Subsystem\<B\> Map\<B\>(Func\<A, B\> f) =\> new Subsystem\<B\>(\_\_comp.Map(f)); public Subsystem\<B\> Select\<B\>(Func\<A, B\> f) =\> new Subsystem\<B\>(\_\_comp.Map(f)); public Subsystem\<B\> SelectMany\<B\>(Func\<A, Subsystem\<B\>\> f) =\> new Subsystem\<B\>(\_\_comp.Bind(a =\> f(a).\_\_comp)); public Subsystem\<C\> SelectMany\<B, C\>(Func\<A, Subsystem\<B\>\> bind, Func\<A, B, C\> project) =\> new Subsystem\<C\>(\_\_comp.Bind(a =\> bind(a).\_\_comp.Map(b =\> project(a, b)))); public Subsystem\<TestBed.IO\> Ask =\> new Subsystem\<TestBed.IO\>(LanguageExt.Prelude.ask\<TestBed.IO\>()); public TryOption\<A\> Run(TestBed.IO env) =\> \_\_comp.Run(env); public Subsystem\<A\> Where(Func\<A, bool\> f) =\> new Subsystem\<A\>(\_\_comp.Where(f)); public Subsystem\<A\> Filter(Func\<A, bool\> f) =\> new Subsystem\<A\>(\_\_comp.Filter(f)); public Subsystem\<A\> Do(Action\<A\> f) =\> new Subsystem\<A\>(\_\_comp.Do(f)); public Subsystem\<A\> Strict() =\> new Subsystem\<A\>(\_\_comp.Strict()); public Seq\<A\> ToSeq(TestBed.IO env) =\> \_\_comp.ToSeq(env); public Subsystem\<LanguageExt.Unit\> Iter(Action\<A\> f) =\> new Subsystem\<LanguageExt.Unit\>(\_\_comp.Iter(f)); public Func\<TestBed.IO, S\> Fold\<S\>(S state, Func\<S, A, S\> f) { var self = this; return env =\> self.\_\_comp.Fold(state, f).Run(env).IfNoneOrFail(state); } public Func\<TestBed.IO, bool\> ForAll\<S\>(S state, Func\<A, bool\> f) { var self = this; return env =\> self.\_\_comp.ForAll(f).Run(env).IfNoneOrFail(false); } public Func\<TestBed.IO, bool\> Exists\<S\>(S state, Func\<A, bool\> f) { var self = this; return env =\> self.\_\_comp.Exists(f).Run(env).IfNoneOrFail(false); } }public static partial class Subsystem{ public static Subsystem\<A\> Return\<A\>(A value) =\> Subsystem\<A\>.Return(value); public static Subsystem\<A\> Fail\<A\>() =\> Subsystem\<A\>.Fail; public static Subsystem\<A\> asks\<A\>(Func\<TestBed.IO, A\> f) =\> new Subsystem\<A\>(env =\> (f(env), false)); public static readonly Subsystem\<TestBed.IO\> ask = new Subsystem\<TestBed.IO\>(env =\> (env, false)); }
    

    This then allows for the example code above to become:

    var comp = from io in Subsystem.asklet ls = io.ReadAllLines("c:/test.txt") let \_= io.WriteAllLines("c:/test-copy.txt", ls) select ls.Count;
    

    ๐Ÿ‘€ As you can see it has provided wrappers for all of the useful functions of the Reader monad into two partial types that can then easily be extended. This makes it much easier to work with the Reader: the generated monad will behave almost exactly like any of the other simpler monads like Option, but will have access to a 'hidden' environment.

    This is the first step to simplifying the use of Reader<Env, A>, Writer<MonoidW, W, A>, State<S, A>, and RWS<MonoidW, R, W, S, A>. The future versions will also generate the Monad class-instance, as well as generate the transformer stack for easy nesting of the new monad.

  • v3.3.0 Changes

    July 17, 2019

    ๐Ÿš€ Language-ext has been in beta for over a month, this is the first full release since. It features:

    ๐Ÿ’ฅ Breaking changes:

    • Seq<A> is now a struct and so can't be null. This mostly isn't a problem for code that needs to check for null to initialise the collection as empty, but if it does something else then that will fail. So, look for uses of Seq<A> and just validate that you'll be ok.
  • v3.2.6 Changes

    July 14, 2019

    ๐Ÿ”’ 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

  • v3.2.6-beta Changes

    July 14, 2019

    ๐Ÿ”’ 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

  • v3.2.4 Changes

    June 24, 2019

    ๐ŸŽ HashSet has been switched to use the same TrieMap that HashMap uses. This will give a similar performance boost to HashSet.

    ๐ŸŽ There have been some more tweaks to TrieMap performance too. Which I'm keeping track of here.

    More to come!

  • v3.2.4-beta Changes

    June 24, 2019

    ๐ŸŽ HashSet has been switched to use the same TrieMap that HashMap uses. This will give a similar performance boost to HashSet.

    ๐ŸŽ There have been some more tweaks to TrieMap performance too. Which I'm keeping track of here.

    More to come!

  • v3.2.2 Changes

    June 23, 2019

    HashMap<K, V> and HashMap<EqK, K, V> are now about 4 times faster. The underlying implemenation has now switched from being a Map<int, Lst<(K, V)> to a Compressed Hash Array Map Trie (CHAMP).

    ๐ŸŽ This still has some more performance to squeeze out of it as it's a relatively naive implementation at the moment, so I won't post up the figures just yet.