chore(deps): update dependency languageext.core to 5.0.0-beta-54 #9
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Note
Mend has cancelled the proposed renaming of the Renovate GitHub app being renamed to
mend[bot]
.This notice will be removed on 2025-10-07.
This PR contains the following updates:
5.0.0-beta-48
->5.0.0-beta-54
Release Notes
louthy/language-ext (LanguageExt.Core)
v5.0.0-beta-54
: Refining the Maybe.MonadIO conceptA previous idea to split the
MonadIO
trait into two traits:Traits.MonadIO
andMaybe.MonadIO
- has allowed monad-transformers to pass IO functionality down the transformer-chain, even if the outer layers of the transformer-chain aren't 'IO capable'.This works as long as the inner monad in the transformer-chain is the
IO<A>
monad.There are two distinct types of functionality in the
MonadIO
trait:MonadIO.LiftIO
)MonadIO.ToIO
andMonadIO.MapIO
)Problem no.1
It is almost always possible to implement
LiftIO
, but it is often impossible to implementToIO
(the minimum required unlifting implementation) without breaking composition laws.Much of the 'IO functionality for free' of
MonadIO
comes from leveragingToIO
(for example,Repeat
,Fork
,Local
,Await
,Bracket
, etc.) -- and so ifToIO
isn't available and has a default implementation that throws an exception, thenRepeat
,Fork
,Local
,Await
,Bracket
, etc. will also all throw.This feels wrong to me.
Problem no.2
Because of the implementation hierarchy:
Methods like
LiftIO
andToIO
, which have default-implementations (that throw) inMaybe.MonadIO<M>
, don't have their overridden implementations enforced when someone implementsMonadIO<M>
. We can just leaveLiftIO
andToIO
on their defaults, which means inheriting fromMonadIO<M>
has no implementation guarantees.Solution
MonadIO
(andMaybe.MonadIO
) into distinct traits:MonadIO
andMaybe.MonadIO
for lifting functionality (LiftIO
)MonadUnliftIO
andMaybe.MonadUnliftIO
for unlifting functionality (ToIO
andMapIO
)StateT
andOptionT
) then we only implementMonadIO
MonadIO
andMonadUnliftIO
.MonadIO
andMonadUnliftIO
(the non-Maybe versions) we makeabstract
the methods that previously had defaultvirtual
(exception throwing) implementations.Maybe.MonadIO
andMaybe.MonadUnliftIO
have the*Maybe
suffix (soLiftIOMaybe
,ToIOMaybe
, etc.)Maybe
variants, but in the code it's declarative, we can see it might not work.MonadIO
andMonadUnliftIO
(the non-Maybe versions) we can overrideLiftIOMaybe
,ToIOMaybe
, andMapIOMaybe
and get them to invoke the bespokeLiftIO
,ToIO
, andMapIO
fromMonadIO
andMonadUnliftIO
.Repeat
,Fork
,Local
,Await
,Bracket
, gets routed to the bespoke IO functionality for the type.The implementation hierarchy now looks like this:
This should (if I've got it right) lead to more type-safe implementations, fewer exceptional errors for IO functionality not implemented, and a slightly clearer implementation path. It's more elegant because we override implementations in
MonadIO
andMonadUnliftIO
, not theMaybe
versions. So, it feels more 'intentional'.For example, this will work, because
ReaderT
supports lifting and unlifting because it implementsMonadUnliftIO
Whereas this won't compile, because
StateT
can only support lifting (by implementingMonadIO
):If you tried to implementing
MonadUnliftIO
forStateT
you quickly run into the fact thatStateT
(when run) yields a tuple, which isn't compatible with the singleton value needed forToIO
. The only way to make it work is to drop the yielded state, which breaks composition rules.Previously, this wasn't visible to the user because it was hidden in default implementations that threw exceptions.
@micmarsh @hermanda19 if you are able to cast a critical eye on this and let me know what you think, that would be super helpful?
I ended up trying a number of different approaches and my eyes have glazed over somewhat, so treat this release with some caution. I think it's good, but critique and secondary eyes would be helpful! That goes for anyone else interested too.
Thanks in advance 👍
v5.0.0-beta-52
: IObservable support in Source and SourceTIObservable
can now be lifted intoSource
andSourceT
types (viaSource.lift
,SourceT.lift
, andSourceT.liftM
).Source
orSourceT
is now supports lifting of the following types:IObservable
IEnumerable
IAsyncEnumerable
System.Threading.Channels.Channel
And, because both
Source
andSourceT
can be converted toProducer
andProducerT
(viaToProducer
andToProducerT
), all of the above types can therefore also be used in Pipes.v5.0.0-beta-51
: LanguageExt.Streaming + MonadIO + DerivingFeatures:
Source
SourceT
Sink
SinkT
Conduit
ConduitT
MonadIO
Deriving
New streaming library
A seemingly innocuous bug in the
StreamT
type opened up a rabbit hole of problems that needed a fundamental rewrite to fix. In the process more and more thoughts came to my mind about bringing the streaming functionality under one roof. So, now, there's a new language-ext libraryLanguageExt.Streaming
and theLanguageExt.Pipes
library has been deprecated.This is the structure of the
Streaming
library:Transducers are back
Transducers were going to be the big feature of
v5
before I worked out the new trait-system. They were going to be too much effort to bring in + all of the traits, but now with the new streaming functionality they are hella useful again. So, I've re-addedTransducer
and a newTransducerM
(which can work with lifted types). Right now the functionality is relatively limited, but you can extend the set of transducers as much as you like by deriving new types fromTransducer
andTransducerM
.Documentation
The API documentation has some introductory information on the streaming functionality. It's a little light at the moment because I wanted to get the release done, but it's still useful to look at:
The
Streaming
library of language-ext is all about compositional streams. There are two key types of streamingfunctionality: closed-streams and open-streams...
Closed streams
Closed streams are facilitated by the
Pipes
system. The types in thePipes
system are compositionalmonad-transformers that 'fuse' together to produce an
EffectT<M, A>
. This effect is a closed system,meaning that there is no way (from the API) to directly interact with the effect from the outside: it can be executed
and will return a result if it terminates.
The pipeline components are:
ProducerT<OUT, M, A>
PipeT<IN, OUT, M, A>
ConsumerT<IN, M, A>
These are the components that fuse together (using the
|
operator) to make anEffectT<M, A>
. Thetypes are monad-transformers that support lifting monads with the
MonadIO
trait only (which constrainsM
). Thismakes sense, otherwise the closed-system would have no effect other than heating up the CPU.
There are also more specialised versions of the above that only support the lifting of the
Eff<RT, A>
effect-monad:Producer<RT, OUT, A>
Pipe<RT, IN, OUT, A>
Consumer<RT, IN, A>
They all fuse together into an
Effect<RT, A>
Pipes are especially useful if you want to build reusable streaming components that you can glue together ad infinitum.
Pipes are, arguably, less useful for day-to-day stream processing, like handling events, but your mileage may vary.
More details on the
Pipes page
.Open streams
Open streams are closer to what most C# devs have used classically. They are like events or
IObservable
streams.They yield values and (under certain circumstances) accept inputs.
Source
andSourceT
yield values synchronously or asynchronously depending on their construction. Can support multiple readers.Sink
andSinkT
receives values and propagates them through the channel they're attached to. Can support multiple writers.Conduit
andConduitT
provides and input transducer (acts like aSink
), an internal buffer, and an output transducer (acts like aSource
). Supports multiple writers and one reader. But can yield aSource
`SourceT` that allows for multiple readers.Source
Source<A>
is the 'classic stream': you can lift any of the following types into it:System.Threading.Channels.Channel<A>
,IEnumerable<A>
,IAsyncEnumerable<A>
, or singleton values. To process a stream, you need to use one of theReduce
or
ReduceAsync
variants. These takeReducer
delegates as arguments. They are essentially a fold over the stream ofvalues, which results in an aggregated state once the stream has completed. These reducers can be seen to play a similar
role to
Subscribe
inIObservable
streams, but are more principled because they return a value (which we can leverageto carry state for the duration of the stream).
Source
also supports some built-in reducers:Last
- aggregates no state, simply returns the last item yieldedIter
- this forces evaluation of the stream, aggregating no state, and ignoring all yielded values.Collect
- adds all yielded values to aSeq<A>
, which is then returned upon stream completion.SourceT
SourceT<M, A>
is the classic-stream embellished - it turns the stream into a monad-transformer that canlift any
MonadIO
-enabled monad (M
), allowing side effects to be embedded into the stream in a principled way.So, for example, to use the
IO<A>
monad withSourceT
, simply use:SourceT<IO, A>
. Then you can use one of thefollowing
static
methods on theSourceT
type to liftIO<A>
effects into a stream:SourceT.liftM(IO<A> effect)
creates a singleton-streamSourceT.foreverM(IO<A> effect)
creates an infinite stream, repeating the same effect over and overSourceT.liftM(Channel<IO<A>> channel)
lifts aSystem.Threading.Channels.Channel
of effectsSourceT.liftM(IEnumerable<IO<A>> effects)
lifts anIEnumerable
of effectsSourceT.liftM(IAsyncEnumerable<IO<A>> effects)
lifts anIAsyncEnumerable
of effectsSourceT
also supports the same built-in convenience reducers asSource
(Last
,Iter
,Collect
).Sink
Sink<A>
provides a way to accept many input values. The values are buffered until consumed. The sink can bethought of as a
System.Threading.Channels.Channel
(which is the buffer that collects the values) that happens tomanipulate the values being posted to the buffer just before they are stored.
So, to manipulate values coming into the
Sink
, useComap
. It will give you a newSink
with the manipulation 'built-in'.SinkT
SinkT<M, A>
provides a way to accept many input values. The values are buffered until consumed. The sink canbe thought of as a
System.Threading.Channels.Channel
(which is the buffer that collects the values) that happens tomanipulate the values being posted to the buffer just before they are stored.
So, to manipulate values coming into the
SinkT
, useComap
. It will give you a newSinkT
with the manipulation 'built-in'.SinkT
is also a transformer that lifts types ofK<M, A>
.Conduit
Conduit<A, B>
can be pictured as so:A
is posted to theConduit
(viaPost
)Transducer
, mapping theA
value toX
(an internal type you can't see)X
value is then stored in the conduit's internal buffer (aSystem.Threading.Channels.Channel
)Reduce
will force the consumption of the values in the bufferX
through the outputTransducer
So the input and output transducers allow for pre and post-processing of values as they flow through the conduit.
Conduit
is aCoFunctor
, callComap
to manipulate the pre-processing transducer.Conduit
is also aFunctor
, callMap
to manipulate the post-processing transducer. There are other non-trait, but common behaviours, likeFoldWhile
,Filter
,Skip
,Take
, etc.ConduitT
ConduitT<M, A, B>
can be pictured as so:K<M, A>
is posted to theConduit
(viaPost
)TransducerM
, mapping theK<M, A>
value toK<M, X>
(an internal type you can't see)K<M, X>
value is then stored in the conduit's internal buffer (aSystem.Threading.Channels.Channel
)Reduce
will force the consumption of the values in the bufferK<M, A>
through the outputTransducerM
So the input and output transducers allow for pre and post-processing of values as they flow through the conduit.
ConduitT
is aCoFunctor
, callComap
to manipulate the pre-processing transducer.Conduit
is also aFunctor
, callMap
to manipulate the post-processing transducer. There are other non-trait, but common behaviours, likeFoldWhile
,Filter
,Skip
,Take
, etc.Open to closed streams
Clearly, even for 'closed systems' like the
Pipes
system, it would be beneficial to be able to post valuesinto the streams from the outside. And so, the open-stream components can all be converted into
Pipes
componentslike
ProducerT
andConsumerT
.Conduit
andConduitT
supportToProducer
,ToProducerT
,ToConsumer
, andToConsumerT
.Sink
andSinkT
supportsToConsumer
, andToConsumerT
.Source
andSourceT
supportsToProducer
, andToProducerT
.This allows for the ultimate flexibility in your choice of streaming effect. It also allows for efficient concurrency in
the more abstract and compositional world of the pipes. In fact
ProducerT.merge
, which merges many streams into one,uses
ConduitT
internally to collect the values and to merge them into a singleProducerT
.MonadIO
Based on this discuission I have refactored
Monad
,MonadIO
, and created a newMaybe.MonadIO
. This achieves the aims of the makingMonadIO
a useful trait and constraint. The one difference between the proposal and my implementation is that I didn't makeMonadT
inheritMonadIO
.Any monad-transformer must add its own
MonadIO
constraint if allowsIO
to be lifted into the transformer. This is more principled, I think. It allows for some transformers to be explicitly non-IO if necessary.All of the core monad-transformers support
MonadIO
-- so the ultimate goal has been achieved.Deriving
Anybody who's used Haskell knows the
deriving
keyword and its ability to provide trait-implementations automatically (for traits likeFunctor
and the like). This saves writing a load of boilerplate. Well thanks to a suggestion by @micmarsh we can now do the same.The technique uses natural-transformations to convert to and from the wrapper type. You can see this in action in the
CardGame
sample. TheGame
trait-implementation looks like this:The only thing that needs implementing is the
Transform
andCoTransform
methods. They simply unpack the underlying implementation or repack it.Deriving.Monad
simply implementsMonad<M>
in terms ofTransform
andCoTransform
, which means you don't have to write all the boilerplate.Conclusion
Can I also just say a personal note of thanks to @hermanda19 and @micmarsh - well worked out and thoughtful suggestions, like the ones listed above, are manna for a library like this that is trying to push the limits of the language. Thank you!
Finally, I will be working on some more documentation and getting back to my blog as soon as I can. This is the home stretch now. So, there's lots of documentation, unit tests, refinements, etc. as I head toward the full
v5
release. I have a few trips lined up, so it won't be imminent, but hopefully at some point in the summer I'll have the full release out of the door!v5.0.0-beta-50
: IO 'acquired resource tidy up' bug-fixThis issue highlighted an acquired resource tidy-up issue that needed tracking down...
The
IO
monad has an internal state-machine. It tries to run that synchronously until it finds an asynchronous operation. If it encounters an asynchronous operation then it switches to a state-machine that uses theasync
/await
machinery. The benefit of this is that we have noasync
/await
overhead if there's no asynchronicity and only use it when we need it.But... the initial synchronous state-machine used a
try
/finally
block that was used to tidy up the internally allocatedEnvIO
(and therefore any acquired resources). This is problematic when switching fromsync -> async
as thetry
/finally
isn't then sequenced correctly.That was a slightly awkward one to track down. Should be fixed now!
Configuration
📅 Schedule: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.