RWST monad transformer
Pre-releaseThe 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.