Releases: louthy/language-ext
Code-gen: Improved namespacing
This is a small update to the LanguageExt.CodeGen
package to improve the namespacing of types and methods in the generated RWS
and Reader
monad code (so you don't have to provide the namespaces manually).
Breaking change: RWS monad now supports Error type
As with the previous release that refactored the Reader
monad to have better error handling. I have now done the same for the RWS
monad.
Breaking changes
RWS
doesn't now return a tuple and instead returnsRWSResult<MonoidW, R, W, S, A>
which has a lot of the same functionality asReaderResult<A>
but with additional functionality bespoke to theRWS
monad (ToReader()
,ToWriter()
,ToState()
).- The old
RWSResult
static class has been replaced and you should now use theRWS
andRWSFail
constructors in thePrelude
to construct the pure and failure monads.
Code-gen
The LanguageExt.CodeGen
library has been updated to work with the new RWS
monad and is the easiest way to work with Reader
and RWS
monads (Writer
and State
will be added soon).
For those that have missed it, this:
namespace TestBed
{
[RWS(WriterMonoid: typeof(MSeq<string>),
Env: typeof(IO),
State: typeof(Person),
Constructor: "Pure",
Fail: "Error" )]
public partial struct Subsys<T>
{
}
}
Will generate:
namespace TestBed
{
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) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>.New(state, value));
public static Subsys<T> Error() => new Subsys<T>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>.New(state, LanguageExt.Common.Error.Bottom));
public static Subsys<T> Error(string message) => new Subsys<T>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>.New(state, LanguageExt.Common.Error.New(message)));
public static Subsys<T> Error(Exception exception) => new Subsys<T>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>.New(state, LanguageExt.Common.Error.New(exception)));
public static Subsys<T> Error(LanguageExt.Common.Error error) => new Subsys<T>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>.New(state, error));
public static Subsys<T> Error(string message, Exception exception) => new Subsys<T>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>.New(state, LanguageExt.Common.Error.New(message, exception)));
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> Error<T>() => Subsys<T>.Error();
public static Subsys<T> Error<T>(string message) => Subsys<T>.Error(message);
public static Subsys<T> Error<T>(string message, Exception exception) => Subsys<T>.Error(message, exception);
public static Subsys<T> Error<T>(Exception exception) => Subsys<T>.Error(exception);
public static Subsys<T> Error<T>(LanguageExt.Common.Error error) => Subsys<T>.Error(error);
public static Subsys<T> asks<T>(Func<TestBed.IO, T> f) => new Subsys<T>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>.New(state, f(env)));
public static Subsys<TestBed.IO> ask => new Subsys<TestBed.IO>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, TestBed.IO>.New(state, env));
public static Subsys<TestBed.Person> get => new Subsys<TestBed.Person>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, TestBed.Person>.New(state, state));
public static Subsys<T> gets<T>(Func<TestBed.Person, T> f) => new Subsys<T>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>.New(state, f(state)));
public static Subsys<LanguageExt.Unit> put(TestBed.Person value) => new Subsys<LanguageExt.Unit>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, LanguageExt.Unit>.New(value, default(LanguageExt.Unit)));
public static Subsys<LanguageExt.Unit> modify(Func<TestBed.Person, TestBed.Person> f) => new Subsys<LanguageExt.Unit>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, LanguageExt.Unit>.New(f(state), default(LanguageExt.Unit)));
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);
}
}
Thereby making it much easier to work with the RWS
monad.
Breaking change: Result and Reader
Reader
The Reader
monad now has more advanced error handling (which is also reflected in the code-gen that wraps the Reader
monad).
This means Reader<Env, A>.Run(env)
now returns ReaderResult<A>
instead of TryOption<A>
. I have also removed Filter
and Where
from Reader
and instead you should use:
from x in success
? Reader<Env, A>(successValue)
: ReaderFail<Env, A>("Fail message")
...
Filter
and Where
would return a result in a Bottom
state, which isn't ideal. Obviously if you need this functionality back then you can create the extension methods yourself to create a similar functionality:
public static Reader<Env, A> Where<Env, A>(this Reader<Env, A> ma, Func<A, bool> f) =>
ma.Bind(a => f(a) ? Reader<Env, A>(a) : ReaderFail<Env, A>(BottomException.Default));
Fail states can be created using:
ReaderFail<Env, A>(string message);
ReaderFail<Env, A>(string message, Exception exception);
ReaderFail<Env, A>(Exception exception);
ReaderResult
ReaderResult<A>
has Match
and IfNone
(so replace previous usage of IfNoneOrFail
with IfNone
). It also has conversion methods: ToSeq()
, ToList()
, ToOption()
, ToOptionUnsafe()
, ToOptionAsync()
, ToEither()
, ToEither(Func<Error, L> f)
, ToEitherUnsafe()
, ToEitherUnsafe(Func<Error, L> f)
, ToEitherAsync()
, ToEitherAsync(Func<Error, L> f)
, ToTry()
, ToTryAsync()
Error
To facilitate the better error handling I needed to add a new Error
type. The chances of this clashing with user's code is large, so it has been put into a new namespace: LanguageExt.Common
. It can hold a message, a status value int
, and an Exception
.
Result
It seems to me that Result
, OptionalResult
, and Error
belong in the same namespace so Result
and OptionalResult
have been moved to LanguageExt.Common
.
On the whole you shouldn't really see Error
or Result
, most of the time you'll just be doing member access - and so the need to include the LanguageExt.Common
should be rare.
HashMap important bug fix patch release #2
Unfortunately the previous fix to the HashMap
and HashSet
didn't catch all cases. This has now been updated and thoroughly tested.
If you're running any release of lang-ext since v3.3.0
(and using HashMap
or HashSet
) then it is advisable that you upgrade as soon as possible.
HashMap important bug fix patch release
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.
RWS monad code generation
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. The state is immutable and so operation is pure.
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.
ReadAllLines
method fromIO
(Reader
environment)WriteAllLines
method fromIO
(Reader
environment)Zero
property fromIO
(Reader
environment)Name
property fromPerson
(State
value)Surname
property fromPerson
(State
value)
So, you can write:
using Subsys;
var fullName = from n in Name
from s in Surname
from _ in WriteAllLines(Seq(n, s))
select $"{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> {}
The usual functions for RWS
are available for access to the underlying state:
from s in get // to get the state-monad state
from _ in put(s) // to put an updated state back into the state-monad
from e in ask // to get the reader-monad environment
from e in asks(io => ...) // easy way to wrap some non-monadic functionality into the monad
from _ in tell(Seq1("Hello")) // to write to the writer-monad when the monoid is a `MSeq<string>`
from _ in modify(s => s) // to modify the state inline
from r in local(ma, e => e) // to provide an alternative environment for the `ma` monad to run in
// this could be considered running in a new scope for the duration
// of the ma operat...
Reader monad code-gen (improvements)
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 theEnv
, 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 x in LiftIO(100)
from y in 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));
}
Reader monad code generation
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.ask
let 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.
Software Transactional Memory & Performance Release
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.
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).