Releases: louthy/language-ext
Iterator: a safe IEnumerator
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
inStreamT
- Potentially use
Iterator
in Pipes - Potentially create
IteratorT
(although this would likely just beStreamT
, so maybe a renaming)
IO refactor continued
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
andRunAsync
methods have been updated to neverawait
. If at any point an asynchronous DSL entry is encountered then processing is deferred toRunAsyncInternal
(which does useawait
). Because,RunAsync
usesValueTask
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>
(seeFinal<F>
)
TODO
- DSL support for
Repeat*
andRetry*
- 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
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 thetailIO
function. This should bring infinite recursion to all types that use theIO
monad (likeEff
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 theSelectMany
used by LINQ has the finalFunc<A, B, C>
argument to invoke after theFunc<A, IO<B>>
monad-bind function (which is the recursive one). TheFunc<A, B, C>
is the trailingselect
and is always needed. It would be good if C# supported aSelectMany
that is more like a regular monadic-bind and recognised the pattern of no additional processing inselect
, 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:
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
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<L, R>
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
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. fromv4
, 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
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
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
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 -- as I was writing this, I realised there was a better way using the existing 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.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
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 aMonoid
Option<A>
made into aMonoid
- The
ToEnumerable
extension forFoldable
has been renamed toToIterable
(it was missed on the previous refactor) StreamT
internals made lazy- Allows the removal of bespoke types that deal only with
IEnumerable
andIAsyncEnumerable
- Allows the removal of bespoke types that deal only with
HeadUnsafe
removed fromStreamT
HeadOrFail
extension toStreamT
added. Only works withFallible
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 aStreamT
- There's an outstanding bug that needs resolving
- You can see how it will work in the
Streams
sample
RWST monad transformer
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.