Skip to content

Releases: louthy/language-ext

Iterator: a safe IEnumerator

25 Dec 20:08
Compare
Choose a tag to compare
Pre-release

Language-ext gained Iterable a few months back, which is a functional wrapper for IEnumerable. We now have Iterator, which is a more functional wrapper for IEnumerator.

IEnumerator is particularly problematic due to its mutable nature. It makes it impossible to share or leverage safely within other immutable types.

For any type where you could previously call GetEnumerator(), it is now possible to call GetIterator().

Iterator is pattern-matchable, so you can use the standard FP sequence processing technique:

public static A Sum<A>(this Iterator<A> self) where A : INumber<A> =>
    self switch
    {
        Iterator<A>.Nil                 => A.Zero,
        Iterator<A>.Cons(var x, var xs) => x + xs.Sum()
    };

Or, bog standard imperative processing:

for(var iter = Naturals.GetIterator(); !iter.IsEmpty; iter = iter.Tail)
{
    Console.WriteLine(iter.Head);
}

You need to be a little careful when processing large lists or infinite streams.. Iterator<A> uses Iterator<A>.Cons and Iterator<A>.Nil types to describe a linked-list of values. That linked-list requires an allocated object per item. That is not really a problem for most of us that want correctness over outright performance, it is a small overhead. But, the other side-effect of this is that if you hold a reference to the head item of a sequence and you're processing an infinite sequence, then those temporary objects won't be freed by the GC. Causing a space leak.

This will cause a space-leak:

var first = Naturals.GetIterator();
for(var iter = first; !iter.IsEmpty; iter = iter.Tail)
{
    Console.WriteLine(iter.Head);
}

first references the first Iterator<A>.Cons and every subsequent item via the Tail.

This (below) is OK because the iter reference keeps being overwritten, which means nothing is holding on the Head item in the sequence:

for(var iter = Naturals.GetIterator(); !iter.IsEmpty; iter = iter.Tail)
{
    Console.WriteLine(iter.Head);
}

This type is probably more useful for me when implementing the various core types of language-ext, but I can't be the only person who's struggled with IEnumerator and its horrendous design.

A good example of where I am personally already seeing the benefits is IO<A>.RetryUntil.

This is the original version:

public IO<A> RepeatUntil(
    Schedule schedule,
    Func<A, bool> predicate) =>
    LiftAsync(async env =>
              {
                  if (env.Token.IsCancellationRequested) throw new TaskCanceledException();
                  var token = env.Token;
                  var lenv  = env.LocalResources;
                  try
                  {
                      var result = await RunAsync(lenv);

                      // free any resources acquired during a repeat
                      await lenv.Resources.ReleaseAll().RunAsync(env);

                      if (predicate(result)) return result;

                      foreach (var delay in schedule.Run())
                      {
                          await Task.Delay((TimeSpan)delay, token);
                          result = await RunAsync(lenv);

                          // free any resources acquired during a repeat
                          await lenv.Resources.ReleaseAll().RunAsync(env);

                          if (predicate(result)) return result;
                      }

                      return result;
                  }
                  finally
                  {
                      // free any resources acquired during a repeat
                      await lenv.Resources.ReleaseAll().RunAsync(env);
                  }
              });      
    

Notice the foreach in there and the manual running of the item to retry with RunAsync. This has to go all imperative because there previously was no way to safely get the IEnumerator of Schedule.Run() and pass it around.

This is what RetryUntil looks like now:

public IO<A> RetryUntil(Schedule schedule, Func<Error, bool> predicate)
{
    return go(schedule.PrependZero.Run().GetIterator(), Errors.None);

    IO<A> go(Iterator<Duration> iter, Error error) =>
        iter switch
        {
            Iterator<Duration>.Nil =>
                IO.fail<A>(error),

            Iterator<Duration>.Cons(var head, var tail) =>
                IO.yieldFor(head)
                  .Bind(_ => BracketFail()
                               .Catch(e => predicate(e)
                                               ? IO.fail<A>(e)
                                               : go(tail, e)))
        };
}

Entirely functional, no imperative anything, and even (potentially) infinitely recursive depending on the Schedule. There's also no manual running of the IO monad with RunAsync, which means we benefit from all of the DSL work on optimising away the async/await machinery.

Future:

  • Potentially use Iterator in StreamT
  • Potentially use Iterator in Pipes
  • Potentially create IteratorT (although this would likely just be StreamT, so maybe a renaming)

IO refactor continued

23 Dec 21:41
Compare
Choose a tag to compare
IO refactor continued Pre-release
Pre-release

IO<A>

The work has continued following on from the last IO refactor release. The previous release was less about optimisation and more about correctness, this release is all about making the async/await state-machines disappear for synchronous operations.

  • The core Run and RunAsync methods have been updated to never await. If at any point an asynchronous DSL entry is encountered then processing is deferred to RunAsyncInternal (which does use await). Because, RunAsync uses ValueTask it's possible to run synchronous processes with next to zero overhead and still resolve to a fully asynchronous expression when one is encountered.
  • The DSL types have all been updated too, to try to run synchronously, if possible, and if not defer to asynchronous versions.
  • DSL state-machine support for resource tracking. It automatically disposes resources on exception-throw
  • DSL support for three folding types: IOFold, IOFoldWhile, IOFoldUntil (see 'Folding')
  • DSL Support for Final<F> (see Final<F>)

TODO

  • DSL support for Repeat* and Retry* - then all core capabilities can run synchronously if they are composed entirely of synchronous components.

Folding

The standard FoldWhile and FoldUntil behaviour has changed for IO (and will change for all FoldWhile and FoldUntil eventually: it dawned on me that it was a bit of a waste that FoldWhile was equivalent to FoldUntil but with a not on the predicate.

So, the change in behaviour is:

  • The FoldUntil predicate test (and potential return) is run after the fold-delegate has been run (so it gets the current-value + the fold-delegate updated-state).
    • This was the previous behaviour
  • The FoldWhile predicate test (and potential return) is run before the fold-delegate is run (so it gets the current-value + the current-state).

The benefit of this approach is that you can stop a fold-operation running if the state is already 'bad' with FoldWhile, whereas with FoldUntil you can exit once the fold-operation makes the state 'bad'. The difference is subtle, but it does give additional options.

Final<F> trait

Final<F> is a new trait-type to support try / finally behaviour. This has been implemented for IO for now. This will expand out to other types later. You can see from the updated implementation of Bracket how this works:

public IO<C> Bracket<B, C>(Func<A, IO<C>> Use, Func<Error, IO<C>> Catch, Func<A, IO<B>> Fin) =>
    Bind(x => Use(x).Catch(Catch).Finally(Fin(x)));

It's still early for this type, but I expect to provide @finally methods that work a bit like the @catch methods.

Unit tests for IO

One of the things that's holding up the full-release of v5 (other than the outstanding bugs in Pipes) is the lack of unit-tests for all of the new functionality. So, I've experimented using Rider's AI assistant to help me write the unit-tests. It's fair to say that it's not too smart, but at least it wrote a lot of the boilerplate. So, once I'd fixed up the errors, it was quite useful.

It's debatable whether it was much quicker or not. But, I haven't really spent any time with AI assistants, so I guess it might just be my inexperience of prompting them. I think it's worth pursuing to see if it can help me get through the unit-tests that are needed for v5.

IO refactor

19 Dec 17:39
Compare
Choose a tag to compare
IO refactor Pre-release
Pre-release

IO

I have refactored the IO<A> monad, which is used to underpin all side-effects in language-ext. The API surface is unchanged, but the inner workings have been substantially refactored. Instead of the four concrete implementations of the abstract IO<A> type (IOPure<A>, IOFail<A>, IOSync<A>, and IOAsync<A>), there is now a 'DSL' of operations deriving from IO<A> which are interpreted in the Run and RunAsync methods (it now works like a Free monad).

The DSL is also extensible, so if you have some behaviours you'd like to embed into the IO interpreter than you can derive from one of these four types.

The benefits of the refactored approach are:

  • Fixes this issue
  • Should have more performance (although not tested fully)
  • Extensible DSL
  • Can support infinite recursion

The last one is a big win. Previously, infinite recursion only worked in certain scenarios. It should work for all scenarios now.

Take a look at this infinite-loop sample:

static IO<Unit> infinite(int value) =>
    from _ in writeLine($"{value}")
    from r in infinite(value + 1)
    select r;

The second from expression recursively calls infinite which would usually blow the stack. However, now the stack will not blow and this example would run forever*.

*All LINQ expressions require a trailing select ..., that is technically something that needs to be invoked after each recursive call to infinite. Therefore, with the above implementation, we get a space-leak (memory is consumed for each loop).

To avoid that when using recursion in LINQ, you can use the tail function:

static IO<Unit> infinite(int value) =>
    from _ in writeLine($"{value}")
    from r in tail(infinite(value + 1))
    select r;

For other monads and monad-transformers that lift the IO monad into their stacks, you can use the tailIO function. This should bring infinite recursion to all types that use the IO monad (like Eff for example).

What tail does is says "We're not going run the select at all, we'll just return the result of infinite". That means we don't have to keep track of any continuations in memory. It also means you should never do extra processing in the select, just return the r as-is and everything will work: infinite recursion without space leaks.

tail is needed because the SelectMany used by LINQ has the final Func<A, B, C> argument to invoke after the Func<A, IO<B>> monad-bind function (which is the recursive one). The Func<A, B, C> is the trailing select and is always needed. It would be good if C# supported a SelectMany that is more like a regular monadic-bind and recognised the pattern of no additional processing in select, but we have to put up with the hand we're dealt.

Not doing work after a tail-call is a limitation of tail-recursion in every language that supports it. So, I'm OK being explicit about it with LINQ. Just be careful to not do any additional processing or changing of types in the select.

Note, if you don't use LINQ and instead use a regular monad-bind operation, then we don't need the tail call at all:

static IO<Unit> infinite(int value) =>
    writeLine($"{value}")
       .Bind(_ => infinite(value + 1));

That will run without blowing the stack and without space-leaks. Below is a chart of memory-usage after 670 million iterations:

image

What's nice about looking a the memory-graph is that, not only is it flat in terms of total-usage (around 26mb), it only ever uses the Gen 0 heap. This is something I've always said about functional-programming. We may we generate a lot of temporary objects (lambdas and the like), but they rarely live long enough to cause memory pressures in higher generations of the heap. Even though this is a very simple sample and you wouldn't expect that much pressure, the benefits of most of your memory usage being in Gen 0 is that you're likely using memory addresses already cached by the CPU -- so the churn of objects is less problematic that is often posited.

StreamT made experimental

I'm not sure how I'm going to fix this issue, so until I have a good idea, StreamT will be marked as [Experimental]. To use it, add this to the top of a file:

#pragma warning disable LX_StreamT

Conclusion

This was a pretty large change, so if you're using the beta in production code, please be wary of this release. And, if you spot any issues, please let me know.

XML documentation updates

17 Dec 20:39
Compare
Choose a tag to compare
Pre-release

One of the most idiotic things Microsoft ever did was to use XML as a 'comment' documentation-format when the language it is documenting is full of <, >, and & characters. I'd love to know who it was that thought this was a good idea.

They should be shunned forever!

Anyway, more seriously, I have ignored the 'well formed' XML documentation warnings, forever. I did so because I consider the readability of comments in code to be the most important factor. So, if I needed a < or a > and it looked OK in the source, then that was good enough for me.

I even built my own documentation generator that understood these characters and knew how to ignore them if the weren't known tags (something Microsoft should have done by now!)

I refused to make something like Either<L, R> turn into Either&lt;L, R&gt;

Anyway, there are places where this is problematic:

  • Inline documentation tooltips in IDEs
  • The new 'formatted' documentation of Rider (maybe VS too, but I haven't used it in ages).

The thing is, I still think the source-code is the most important place for readability, so requests like this I ignored until I could think of a better solution. Now I have a better solution which is to use alternative unicode characters that are close enough to <, >, and &, that they read naturally in source and in documentation, but are also not XML delimiters:

Original Replacement Unicode Example (before) Example (after)
< U+3008 Either<Error, A> Either〈Error, A〉
> U+3009 Either<Error, A> Either〈Error, A〉
& U+FF06 x && y x && y

The spacing isn't perfect and in certain situations they look a touch funky, but still legible, so I'm happy to go with this as it makes the documentation work everywhere without compromise. It's slightly more effort for me, but I'd rather do this than compromise the inline comments.

Over 120 source files have been updated with the new characters. There are now no XML documentation errors (and I've removed the NoWarn that suppressed XML documentation errors).

There may well be other unicode characters that are better, but at least now it's a simple search & replace if I ever decided to change them.

Generalised Partition for all Fallible monads

07 Nov 23:19
Compare
Choose a tag to compare

In this release there are now generalised Partition, Succs, and Fails methods (and their equivalent Prelude functions: partition, succs, and fails) that work with any Foldable of Fallible monads.

This is the main Partition extension:

  public static K<M, (Seq<Error> Fails, Seq<A> Succs)> Partition<F, M, A>(
      this K<F, K<M, A>> fma)
      where M : Monad<M>, Fallible<M>
      where F : Foldable<F> =>
      fma.Fold(M.Pure((Fails: Seq.empty<Error>(), Succs: Seq.empty<A>())),
               ma => ms => ms.Bind(
                         s => ma.Bind(a => M.Pure((s.Fails, s.Succs.Add(a))))
                                .Catch(e => M.Pure((s.Fails.Add(e), s.Succs)))));

So, if your F is a Iterable and your M is an IO (so Iterable<IO<A>>), then you can run Partition on that to get a IO<(Seq<Error> Fails, Seq<A> Succs)>. Obviously this will work with any Foldable types you have made too (as well as all the built-in ones: Arr, Lst, Iterable, Seq, Set, HashSet, etc.) -- with any effect type as long as its Fallible and a Monad. Including Fallible types with a bespoke E error value (but you will have to specify the generics as it can't infer from the arguments alone).

For those who know the extensions Partition, Some, Rights, etc. from v4, this generalises the idea completely.

I have also added a Partition extension and partition prelude function that works with any Foldable structure. Unlike the Partition function that works with Fallible types (which partitions on success or failure), this one takes a predicate which is used to partition based on the true / false return. It's like Filter but instead of throwing away the false values, it keeps them and returns a True sequence and False sequence:

public static (Seq<A> True, Seq<A> False) Partition<T, A>(this K<T, A> ta, Func<A, bool> f)
    where T : Foldable<T> =>
    T.Partition(f, ta);

All foldables get a default implementation, but it's possible to override in the trait-implementation (for performance reasons mostly).

Finally, I've added unit-tests for the Foldable default-implementations. Anybody who's been following along will know that the Foldable trait only has two methods that need implementing and over 50 default implementations that we get for free. So, the unit test makes sure they work! (which should guarantee they work for all foldables as long as the two required method-implementations are correct).

I'm considering how I can refactor those unit-tests into a FoldableLaw type that will work like MonadLaw, FunctorLaw, etc. But that's not there yet.

IO monad applicative error collecting

06 Nov 18:28
Compare
Choose a tag to compare
Pre-release

I have added support for the IO<A> monad to collect multiple errors during an applicative Apply call. That also means anything that lifts the IO monad (i.e. transformer stacks or the Eff monad) also get this behaviour. So, you get Validation-like error collection.

Also, bug/missing-feature fixes:

Choice and Alternative traits refactor

28 Oct 11:05
Compare
Choose a tag to compare
Pre-release

Two new traits have been added:

Choice<F>

public interface Choice<F> : Applicative<F>, SemigroupK<F>
    where F : Choice<F>
{
    static abstract K<F, A> Choose<A>(K<F, A> fa, K<F, A> fb);
    
    static K<F, A> SemigroupK<F>.Combine<A>(K<F, A> fa, K<F, A> fb) => 
        F.Choose(fa, fb);
}

Choice<F> allows for propagation of 'failure' and 'choice' (in some appropriate sense, depending on the type).

Choice is a SemigroupK, but has a Choose method, rather than relying on the SemigroupK.Combine method, (which now has a default implementation of invoking Choose). That creates a new semantic meaning for Choose, which is about choice propagation rather than the broader meaning of Combine. It also allows for Choose and Combine to have separate implementations depending on the type.

The way to think about Choose and the inherited SemigroupK.Combine methods is:

  • Choose is the failure/choice propagation operator: |
  • Combine is the concatenation/combination/addition operator: +

Any type that supports the Choice trait should also implement the | operator, to enable easy choice/failure propagation. If there is a different implementation of Combine (rather than accepting the default), then the type should also implement the + operator.

ChoiceLaw can help you test your implementation:

choose(Pure(a),   Pure(b))  = Pure(a)
choose(Fail,      Pure(b))  = Pure(b)
choose(Pure(a),   Fail)     = Pure(a)
choose(Fail [1],  Fail [2]) = Fail [2]

It also tests the Applicative and Functor laws.

Types that implement the Choice trait:

  • Arr<A>
  • HashSet<A>
  • Iterable<A>
  • Lst<A>
  • Seq<A>
  • Either<L, R>
  • EitherT<L, M, R>
  • Eff<A>
  • Eff<RT, A>
  • IO<A>
  • Fin<A>
  • FinT<M, A>
  • Option<A>
  • OptionT<M, A>
  • Try<A>
  • TryT<M, A>
  • Validation<F, A>
  • Validation<F, M, A>
  • Identity<A>
  • IdentityT<M, A>
  • Reader<E, A>
  • ReaderT<E, M, A>
  • RWST<R, W, S, M, A>
  • State<S, A>
  • StateT<S, M, A>
  • Writer<A>
  • WriterT<M, A>

NOTE: Some of those types don't have a natural failure value. For the monad-transformers (like ReaderT, WriterT, ...) they add a Choice constraint on the M monad that is lifted into the transformer. That allows for the Choice and Combine behaviour to flow down the transformer until it finds a monad that has a way of handling the request.

For example:

var mx = ReaderT<Unit, Seq, int>.Lift(Seq(1, 2, 3, 4, 5));
var my = ReaderT<Unit, Seq, int>.Lift(Seq(6, 7, 8, 9, 10));
var mr = mx + my;

ReaderT can't handle the + (Combine) request, so it gets passed down the transformer stack, where the Seq handles it. Resulting in:

ReaderT(Seq(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))

Similarly for |:

var mx = ReaderT<Unit, Option, int>.Lift(Option<int>.None);
var my = ReaderT<Unit, Option, int>.Lift(Option<int>.Some(100));
var mr = mx | my;

The Option knows how to handle | (Choose) and propagates the failure until it gets a Some value, resulting in:

ReaderT(Some(100))

This is quite elegant I think, but it requires all monads in a stack to implement Choice. So, a good sensible default (for regular monads without a failure state), is to simply return the first argument (because it always succeeds). That allows all monads to be used in a transformer stack. This isn't ideal, but it's pragmatic and opens up a powerful set of features.

Alternative<F>

Alternative is a Choice with an additional MonoidK. That augments Choice with Empty and allows for a default empty state.

AlternativeLaw can help you test your implementation:

choose(Pure(a), Pure(b)) = Pure(a)
choose(Empty  , Pure(b)) = Pure(b)
choose(Pure(a), Empty  ) = Pure(a)
choose(Empty  , Empty  ) = Empty

It also tests the Applicative and Functor laws.

Types that implement the Alternative trait:

  • Arr<A>
  • HashSet<A>
  • Iterable<A>
  • Lst<A>
  • Seq<A>
  • Eff<A>
  • Eff<RT, A>
  • IO<A>
  • Fin<A>
  • FinT<M, A>
  • Option<A>
  • OptionT<M, A>
  • Try<A>
  • TryT<M, A>
  • Validation<F, A>
  • Validation<F, M, A>

Thanks to @hermanda19 for advocating for the return of the Alternative trait, I think I'd gotten a little too close to the code-base and couldn't see the wood for the trees when I removed it a few weeks back. The suggestion to make the trait have a semantically different method name (Choose) re-awoke my brain I think! :D

Any thoughts or comments, please let me know below.

Pipes factored out + EnvIO.FromToken + Applicative Zip

25 Oct 09:49
Compare
Choose a tag to compare

The Pipes functionality that is based on the Haskell Pipes library has been factored out to its own library: LanguageExt.Pipes. This is so I can add more functionality to it (based on the supplementary libraries in the Haskell ecosystem). It also creates a clear demarcation: stating that it's not an 'essential'. I think this makes sense because Pipes is quite advanced and currently not the easiest thing to use for those new to FP in C#.

I have also added EnvIO.FromToken, which allows you to construct an EnvIO that subscribes to the token provided. This is useful if you need to use IO within an existing async workflow, rather than at the edge. -- as I was writing this, I realised there was a better way using the existing EnvIO.New, so this has gone already!

The various bespoke zip functions and extensions have now been generalised to work with any applicative. So zips all round!

`Sys.IO` improvements / Triaged issues + fixes release

22 Oct 14:07
Compare
Choose a tag to compare

Sys.IO improvement

The Sys.IO functions were generalised to support any monad that has the correct Has traits. However, this added a burden to any users of Eff and Aff from v4 of language-ext.

For example, this in v4:

    Console<RT>.writeLine(text)

Would become this in v5:

    Console<Eff<RT>, RT>.writeLine(text)

That works well when you're write entirely generalised IO code, like you see in the Newsletter sample project.

Writing this:

    Console<M, RT>.writeLine(text)

Is clearly not much worse than before. And adds lots of possibilities for writing your IO functions once and have them work with all IO based monads. But, this Console<Eff<RT>, RT>.writeLine(text) is quite ugly to look at in my humble opinion. There's too much clutter and it shows up on every line of IO you write, which doesn't feel good.

So, as Eff<RT, A> is an important type for doing dependecy-injection based IO, I have added Eff specific versions for every static class in the Sys library.

So, for example, as well as Console<M, RT>, there is also a Console<RT> which only works with the Eff<RT, A> monad. This allows existing code that uses Eff<RT, A> to work without a change.

I'm not a huge fan of doubling up the work like this, but I think this is a pragmatic solution that works without much drama.

Fixes and Updates:

  • Unit made into a Monoid
  • Option<A> made into a Monoid
  • The ToEnumerable extension for Foldable has been renamed to ToIterable (it was missed on the previous refactor)
  • StreamT internals made lazy
    • Allows the removal of bespoke types that deal only with IEnumerable and IAsyncEnumerable
  • HeadUnsafe removed from StreamT
    • HeadOrFail extension to StreamT added. Only works with Fallible monads.
  • Fix for FileIO move missing? #1174
  • Removed MemoryFS, replaced with use of /tmp as a virtual file-system. Fixes:

Additions, but not ready for primetime

  • Source<A> - a source of events that can be subscribed to. Subscriptions yield a StreamT

RWST monad transformer

17 Oct 22:05
Compare
Choose a tag to compare
Pre-release

The new RWST<R, W, S, M, A> monad-transformer is now fully-featured and ready for real-world use.

For the uninitiated, the RWST monad-transformer combines all of the effects of the Reader, Writer, State, and M monads into a single monad. You could imagine a type like this:

    ReaderT<R, WriterT<W, StateT<S, M>>, A> 

Which stacks three monad-transformers and the monad M into one type. The problem with that is too much transformer stacking leads to lots of nested lambdas. The RWST monad-transformer smushes the Reader/Writer/State into a single layer making it more performant.

You can use Unit for any of the type parameters if you only need two of the three capabilities. For example, if you only need reader and state effects:

    RWST<R, Unit, S, M, A>

Or, reader and writer effects, but not state:

    RWST<R, W, Unit, M, A>

etc.

There's next to no overhead for doing so.

It's also worth noting that RWST is a very common mega-monad for constructing domain-specific monads for applications. And so, even though it's generics heavy, you would normally wrap it up in a type that reduces the generics overhead.

Let's say we wanted to create an App monad-transformer. Something that carries app-config, app-state, but can also lift other monads into it.

First, create some records to hold the config and the state:

public record AppConfig(int X, int Y);

public record AppState(int Value)
{
    public AppState SetValue(int value) =>
        this with { Value = value };
}

Then create your App monad-transformer type. It is simply a record that contains a RWST monad-transformer:

public readonly record struct App<M, A>(RWST<AppConfig, Unit, AppState, M, A> runApp) : K<App<M>, A>
    where M : Monad<M>, SemigroupK<M>
{
    // Your application monad implementation
}

Then add some extensions to convert from the K type to the concrete type and to Run the App monad-transformer:

public static class App
{
    public static App<M, A> As<M, A>(this K<App<M>, A> ma)
        where M : Monad<M>, SemigroupK<M> =>
        (App<M, A>)ma;
    
    public static K<M, (A Value, AppState State)> Run<M, A>(this K<App<M>, A> ma, AppConfig config, AppState state)
        where M : Monad<M>, SemigroupK<M> =>
        ma.As().runApp.Run(config, state).Map(
            ma => ma switch
                  {
                      var (value, _, newState) => (value, newState)
                  });
}

This also drops the Unit output from the RWST.

Then implement the traits for App<M>. It should be a MonadT because it's a monad-transformer; a Readable because it's got an AppConfig we can read with ask; and Stateful because it's got an AppState that we get, gets, modify, and put:

public class App<M> : 
    MonadT<App<M>, M>,
    Readable<App<M>, AppConfig>,
    Stateful<App<M>, AppState>
    where M : Monad<M>, SemigroupK<M>
{
    public static K<App<M>, B> Bind<A, B>(K<App<M>, A> ma, Func<A, K<App<M>, B>> f) =>
        new App<M, B>(ma.As().runApp.Bind(x => f(x).As().runApp));

    public static K<App<M>, B> Map<A, B>(Func<A, B> f, K<App<M>, A> ma) => 
        new App<M, B>(ma.As().runApp.Map(f));

    public static K<App<M>, A> Pure<A>(A value) => 
        new App<M, A>(RWST<AppConfig, Unit, AppState, M, A>.Pure(value));

    public static K<App<M>, B> Apply<A, B>(K<App<M>, Func<A, B>> mf, K<App<M>, A> ma) => 
        new App<M, B>(mf.As().runApp.Apply(ma.As().runApp));

    public static K<App<M>, A> Lift<A>(K<M, A> ma) => 
        new App<M, A>(RWST<AppConfig, Unit, AppState, M, A>.Lift(ma));

    public static K<App<M>, A> Asks<A>(Func<AppConfig, A> f) => 
        new App<M, A>(RWST<AppConfig, Unit, AppState, M, A>.Asks(f));

    public static K<App<M>, A> Local<A>(Func<AppConfig, AppConfig> f, K<App<M>, A> ma) => 
        new App<M, A>(ma.As().runApp.Local(f));

    public static K<App<M>, Unit> Put(AppState value) => 
        new App<M, Unit>(RWST<AppConfig, Unit, AppState, M, Unit>.Put(value));

    public static K<App<M>, Unit> Modify(Func<AppState, AppState> modify) => 
        new App<M, Unit>(RWST<AppConfig, Unit, AppState, M, Unit>.Modify(modify));

    public static K<App<M>, A> Gets<A>(Func<AppState, A> f) => 
        new App<M, A>(RWST<AppConfig, Unit, AppState, M, A>.Gets(f));
}

Every member is simply a wrapper that calls the underlying RWST monad-transformer (which does all the hard work).

Then we can use our new App monad-transformer:

var app = from config in Readable.ask<App<IO>, AppConfig>()
          from value  in App<IO>.Pure(config.X * config.Y)
          from _1     in Stateful.modify<App<IO>, AppState>(s => s.SetValue(value)) 
          from _2     in writeLine(value) 
          select unit;

This leverages the Readable trait to get at the AppConfig, the leverages the Stateful trait to modify the AppState, and finally does some IO (the lifted M monad), by calling writeLine.

That's great and everything, but we want to make the underlying type disappear completely. So, if we then wrap up the Readable.ask and Stateful.modify in App specific functions:

public static App<M, AppConfig> config<M>() 
    where M : Monad<M>, SemigroupK<M> =>
    Readable.ask<App<M>, AppConfig>().As();

public static App<M, Unit> modify<M>(Func<AppState, AppState> f) 
    where M : Monad<M>, SemigroupK<M> =>
    Stateful.modify<App<M>, AppState>(f).As();

Then we can make the resulting code completely App-centric:

var app = from config in App.config<IO>()
          from value  in App<IO>.Pure(config.X * config.Y)
          from _1     in App.modify<IO>(s => s.SetValue(value)) 
          from _2     in writeLine(value) 
          select unit;

So, by simply wrapping up the RWST monad-transformer you can gain a ton of functionality without worrying how to propagate state, log changes (if you use the Writer part of RWST), or manage configuration. Very cool.

Just for completeness, this is what writeLine looks like:

static IO<Unit> writeLine(object value) =>
    IO.lift(() => Console.WriteLine(value));

I'll cover this topic in more detail in the next article of my Higher Kinds series on my blog.