Changelog History
Page 3
-
v3.3.11 Changes
August 15, 2019๐ This is an important fix of an issue with the new
HashMap
andHashSet
implementation (to patch any release sincev3.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 aSeq<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 theReader
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 evenS
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 anS
state that can vary (by callingput(x)
). Below, is a version that doesn't have a variableS
generic parameter, and instead has a fixed state embedded in the wrappedRWS
monad. So,put
can only be called with anotherPerson
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
andPerson
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 inUnit
orMUnit
for the types. This is aState
andReader
monad with no usefulWriter
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 theEnv
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 theEnv
, it can be any type.It is also possible to specify the name of the constructor and failure functions:
Return
andFail
.[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 theReader<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 twopartial
types that can then easily be extended. This makes it much easier to work with theReader
: the generated monad will behave almost exactly like any of the other simpler monads likeOption
, 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>
, andRWS<MonoidW, R, W, S, A>
. The future versions will also generate theMonad
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:
- ๐ Performance improvements for
Seq<A>
,HashMap<A>
, andHashSet<A>
- ๐ Improved perfomance and reduced memory usage by
Option<A>
,OptionUnsafe<A>
. - Software Transcational Memory using Multi-Version Concurrency Control
Atom<A>
Ref<A>
๐ฅ Breaking changes:
Seq<A>
is now astruct
and so can't benull
. This mostly isn't a problem for code that needs to check fornull
to initialise the collection as empty, but if it does something else then that will fail. So, look for uses ofSeq<A>
and just validate that you'll be ok.
- ๐ Performance improvements for
-
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 likelock
(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 functionswap
) 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 theSwap
function, or at the very least it needs to be reliably repeatable.Validation
โก๏ธ The
Atom
andAtomRef
constructors can take aFunc<A, bool>
validation lambda. This is run against the initial value and all subsequent values before being swapped. If the validation lambda returnsfalse
for the proposed value thenfalse
is returned fromSwap
and no update takes place.Events
The
Atom
andAtomRef
types all have aChange
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 theatom.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 theSwap
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 theimplicit operator
conversion toA
.Pool
๐ The updates to the object pooling system also improves the performance of the
Lst
,Set
, andMap
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 uglyConcurrentDictionary
or similar. I will perhaps take a look at theref
system in Clojure too - which is a mechanism for atomic updates of multiple items (STM essentially). -
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 likelock
(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 functionswap
) 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 theSwap
function, or at the very least it needs to be reliably repeatable.Validation
โก๏ธ The
Atom
andAtomRef
constructors can take aFunc<A, bool>
validation lambda. This is run against the initial value and all subsequent values before being swapped. If the validation lambda returnsfalse
for the proposed value thenfalse
is returned fromSwap
and no update takes place.Events
The
Atom
andAtomRef
types all have aChange
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 theatom.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 theSwap
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 theimplicit operator
conversion toA
.Pool
๐ The updates to the object pooling system also improves the performance of the
Lst
,Set
, andMap
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 uglyConcurrentDictionary
or similar. I will perhaps take a look at theref
system in Clojure too - which is a mechanism for atomic updates of multiple items (STM essentially). -
v3.2.4 Changes
June 24, 2019๐
HashSet
has been switched to use the sameTrieMap
thatHashMap
uses. This will give a similar performance boost toHashSet
.๐ 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 sameTrieMap
thatHashMap
uses. This will give a similar performance boost toHashSet
.๐ 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, 2019HashMap<K, V>
andHashMap<EqK, K, V>
are now about 4 times faster. The underlying implemenation has now switched from being aMap<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.