Skip to content

Releases: louthy/language-ext

Code-gen: Improved namespacing

17 Sep 15:06
Compare
Choose a tag to compare

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

15 Sep 18:42
Compare
Choose a tag to compare

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 returns RWSResult<MonoidW, R, W, S, A> which has a lot of the same functionality as ReaderResult<A> but with additional functionality bespoke to the RWS monad (ToReader(), ToWriter(), ToState()).
  • The old RWSResult static class has been replaced and you should now use the RWS and RWSFail constructors in the Prelude 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

21 Aug 12:13
Compare
Choose a tag to compare

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

18 Aug 18:54
Compare
Choose a tag to compare

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

15 Aug 20:56
Compare
Choose a tag to compare

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

14 Aug 15:39
Compare
Choose a tag to compare

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. 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 from IO (Reader environment)
  • WriteAllLines method from IO (Reader environment)
  • Zero property from IO (Reader environment)
  • Name property from Person (State value)
  • Surname property from Person (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...
Read more

Reader monad code-gen (improvements)

12 Aug 20:55
Compare
Choose a tag to compare

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 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

08 Aug 00:03
Compare
Choose a tag to compare

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

17 Jul 10:36
Compare
Choose a tag to compare

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.

New feature: Atom - shared, synchronous, independent state without locks

14 Jul 01:50
Compare
Choose a tag to compare

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