louthy / language-ext

C# functional language extensions - a base class library for functional programming
MIT License
6.49k stars 417 forks source link

Question on composing Readers with monadic bound values #1383

Closed alexmorask closed 6 days ago

alexmorask commented 1 week ago

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

public static Reader<ItemRepository, Eff<Item>> GetItem(ItemId itemId)
    => new (itemRepository => liftEff(async () => await itemRepository.GetById(itemId))); 

And I have another method I want to write with a signature like:

public static Reader<ItemRepository, Eff<Unit>> ProcessItem(ItemId itemId)

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 an Item and results in an Eff<Unit>.

Is it possible to do this using the from .. in .. select syntax that unwraps not only the Reader, but also the resulting Eff?

I initially tried something like:

var result = // Ideally Reader<ItemRepository, Eff<Unit>>
    from effect in GetItem(itemId) // Eff<Item>
    from item in effect // This doesn't work because my Bind context is for `Reader`, not `Eff`
    from _ in ProcessItem(item) // Eff<Unit>
    select _;

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!

louthy commented 1 week 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:

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.

alexmorask commented 1 week ago

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.

louthy commented 1 week ago

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.

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.

alexmorask commented 1 week ago

Awesome, makes perfect sense! Thanks again for the assistance and the detail.