Closed alexmorask closed 6 days ago
LanguageExt is a life-saver.
Thank you :-)
So yeah, you've bumped into the lack of monad-transformers in v4
. The only way to do it in v4
is to unwrap the Eff
monads, as you are doing, then create a sub-expression that works solely with the Eff
monads, which is a bit awkward for how you have it written there.
This is the process (really avoid it though):
public static Reader<ItemRepo, Eff<Unit>> Example(ItemId id) =>
GetItem(id)
.Bind(effect =>
Reader<ItemRepo, Eff<Unit>>(env =>
effect.Bind(
item => ProcessItem(item).Run(env).IfFail(unitEff))));
So, we run the outer monad (Reader
), to get the Eff<Item>
effect. We then need to construct a new Reader
and lift a new effect in to it. Within that we Run
the ProcessItem
result (another Reader
), which gives us access to the Eff<Unit>
within. This process is manually implementing monad-bind for a nested monad, which is what we want to avoid.
Simply, different flavoured monads are not composable. You must group them into single-flavour expressions. Or, create 'super monads' that contain the effects of all the different flavours.
One such 'super monad' is Eff<RT, A>
. Which is literally Reader<RT, Eff<A>>
but packaged up into a single type. The RT
type is used as a runtime for dependency-injection usually, but can be anything you like, like ItemRepository
.
If you're new to language-ext then I'd probably advise going straight to v5
. It's in beta right now, but stable. I'm on the final stretch to full RTM soon.
The big benefit of v5
is that it now supports higher-kinded traits. I have a whole series about it on my blog. These traits enable monad transformers, which allow the 'stacking' of monadic effects (as you are trying to do by combining Reader
and Eff
. But this actually works ;-)
In v5
your example would look like this:
public static ReaderT<ItemRepo, Eff, Item> GetItem(ItemId itemId) =>
from repo in ReaderT.ask<Eff, ItemRepo>()
from item in liftIO(async () => await repo.GetById(itemId))
select item;
public static ReaderT<ItemRepo, Eff, Unit> ProcessItem(Item item) =>
throw new NotImplementedException();
public static ReaderT<ItemRepo, Eff, Unit> Example(ItemId id) =>
from item in GetItem(id)
from _ in ProcessItem(item)
select unit;
Which, as you can see from the final function, allows for both Reader
and Eff
effects in a single LINQ expression without any 'type gymnastics'.
The blog is a good guide to getting moving, but if you want to see a real-world example that does a similar thing to what you're trying, the take a look at the CardGame
sample:
Game.cs
is the main entry point for the game. You can see it uses a bespoke monad called Game<A>
.Game<A>
is defined in the Game.Monad
folderGame.Monad/Game.cs
you can see how it wraps up a StateT<GameState, OptionT<IO>, A>
property. That is two stacked monad-transformers: StateT
and OptionT
, as well as an IO
monad. So, three different effects (state, optionality, IO side-effects) are wrapped into a single type.Game.Monad/Game.Monad.cs
you see the implementation of the traits (as introduced in the blog). They're all just wrapper methods that call the underlying StateT<GameState, OptionT<IO>, A>
type's behaviours.Game.Monad/Game.Module.cs
defines game-specific functions that work with the Game<A>
monad, to create a friendly surface to the stacked-effects.This approach allows you to take the various monadic effects, glue them together, and then make a friendly domain-specific type that represents a subsystem in your application. It also cuts down on the amount of generics you need to type and allows for easy augmentation of functionality later (refactor friendly).
This is a huge change for v5
and is a game-changer for using monadic types in C#. With v4
you're limited to the set of monads I've written, whereas v5
I've created the toolkit that allows you to construct your own.
I hope that helps.
This is incredibly helpful and I greatly appreciate the detailed response! I actually am using V5 already, so this will make my life a lot easier as the concept makes a lot more sense now. I'll reference the CardGame
sample to take this the rest of the way.
Just out of curiosity though if you don't mind, is what I just ran into basically the same practical concept behind a construct like TaskResult<>
in F#'s FsToolkit.ErrorHandling package? I know F# doesn't have higher-kinded types, but the pattern seems very similar albeit provided through computation expressions rather than monad-transformers.
I haven't used TaskResult<>
, but from that link it looks like the old EitherAsync<L, R>
from v4
. That's been dropped now because, yes, this new monad-transformers system is the same concept, but allows us to build composed types on-the-fly rather than having to manually compose every variant of monad effect into one.
If you spend any time in Haskell, you'll see the transformers used a lot. Usually a project will build a set of app-specific monads that represent bits of the domain (
Db<A>
,UI<A>
, or whatever). That allows for demarcation of whole areas of code; allows for things like bespoke security, or state, or config, or whatever. And also makes it easy to refactor later (if you add an effect to a domain-monad then it's the trait implementation that changes, not the code).
So, instead of EitherAsync<L, R>
from v4
(which combines a Left
alternative-value effect and the IO effect of Task
), we now write: EitherT<L, IO, R>
. Which lifts the IO<A>
monad into the EitherT
monad-transformer.
There are lots of transformer-types (see list below). You can stack as many as you like, so the possible combinations of effects is huge. You can roll your own too, you just need to implement MonadT
in your trait-implementation.
Proxy<UOut, UIn, DIn, DOut, M, A>
(the base-type of the Pipes system)StreamT<M, A>
- streams lazy effectsEitherT<L, M, R>
- transformer version of Either<L, R>
FinT<M, A>
- transformer version of Fin<A>
OptionT<M, A>
- transformer version of Option<A>
TryT<M, A>
- transformer version of Try<A>
ValidationT<F, M, A>
- transformer version of Validation<F, A>
ContT<R, M, A>
- continuations (still very WIP, don't use yet!)IdentityT<M, A>
- identity transformer, does nothing but lift an M
ReaderT<E, M, A>
- transformer version of Reader<E, A>
WriterT<W, M, A>
- transformer version of Writer<W, A>
StateT<S, M, A>
- transformer version of State<S, A>
RWST<R, W, S, M, A>
- combines the effects of Reader
, Writer
, State
, and M
into a single type. In theory not necessary, but it performs better than stacking those effects manually.In future there will be a ParserT
too, which will be like the Parser
type in LanguageExt.Parsec
, but with the ability to lift other effects into it.
When you think of all the ways you could combine those types with the regular monads, it's clear it'd be impossible to manually compose all of those variants by hand. I think it would get pretty tedious too: ReaderWriterValidationAsync
is not a pretty name!
It's best to build the two or three stacks your application needs, then create your domain monads from them.
Awesome, makes perfect sense! Thanks again for the assistance and the detail.
First off, @louthy, thanks for all you do with LanguageExt! As a .NET developer who's really enjoying functional programming, but has rarely found work in F#, LanguageExt is a life-saver.
My specific question has to do with composing Readers, but I'm sure there's a higher level concept I'm missing so I figure it's best to ask here.
Let's say I have the following method (I'll try to keep the example as simple as possible):
And I have another method I want to write with a signature like:
I'd like this second method to use the initial
GetItem
method to retrieve the item and then perform some operation on it that takes anItem
and results in anEff<Unit>
.Is it possible to do this using the
from .. in .. select
syntax that unwraps not only theReader
, but also the resultingEff
?I initially tried something like:
Sorry if the example is a bit non-sensical for this application, I'm really just more-so trying to understand the fundamental concepts at play, so any examples or assistance you can point to would be greatly appreciated.
Thanks in advance!