chore(deps): update dependency languageext.core to 5.0.0-beta-57 #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.
This PR contains the following updates:
5.0.0-beta-48->5.0.0-beta-57Release Notes
louthy/language-ext (LanguageExt.Core)
v5.0.0-beta-57: Extension operators supported for more typesFollowing on from the extension-operators and .NET 10 release yesterday, I have now implemented operators for even more of the core types in language-ext.
The full set so far:
ChronicleT<Ch, M, A>Eff<A>Eff<RT, A>Either<L, R>EitherT<L, M, R>Fin<A>FinT<M, A>IO<A>Option<A>OptionT<M, A>These<A, B>Try<A>TryT<M, A>Validation<F, A>ValidationT<F, M, A>There is no need to do any of these to make the extension operators work for the types, but the generic extensions will all return an abstract
K<F, A>type. So, I am in the process of making these bespoke versions return a concrete type. This is purely for usability's sake.And because each core type can support many different traits, the number of operators they support can be quite large too (see the
Tryoperators for a good example!). So, for each core type I've decided on a new folder structure:This will keep the many method extensions and operator extensions away from the core functionality for any one type: which hopefully will make the API docs easier to read and the source-code easier to navigate.
Let me know if you see any quirkiness with the new operators. This is a lot of typing, so it would be good to catch issues early!
v5.0.0-beta-56: .NET 10 version bump + new extension operatorsThe new version of .NET, version 10.0, has added capabilities for 'extension everything'. Well, almost everything. Most importantly for us is that it has added extension support for operators. And not just operators, but generic operators on interfaces and delegates!
I wasn't sure how far the csharplang team had taken it, but it turns out: just far enough to give use some reeeeeally useful capabilties.
This is an early release that:
FunctoroperatorsApplicativeoperatorsMonadoperatorsChoiceoperatorsFallibleoperatorsFinaloperatorsSemigroupoperatorsSemigroupKoperators.As()extension method)If your types implement any of the above traits then you get the operators for free.
Functor>>>
FunctoroperatorsIn a language like Haskell, you will often see something like this:
Where the left-hand side in the mapping function for the
mxfunctor type.With this release of language-ext we can now do the same:
For example, let's create an
IOoperation that reads a line from the console:We can then map the
IO<string>to anIO<Option<int>>like so:Which is the less terse version of this:
Which is ultimately equivalent to:
I have chosen the
*operator as this is applying each value inmxto the function and re-wrapping. Like a cross-product.Applicative>>>
ApplicativeoperatorsThe last example might not have seemed too compelling, but we can also do multi-argument function mapping using a combination of
FunctorandApplicative.Going back to the Haskell example, you could use both functor map and applictive apply together to apply multiple arguments to a mapping function:
The above allows three applicative structures (like
Option,IO,Either,Validation, etc.) to have their logic run to extract the values to pass to the lambda, which sums the values.We can do the same:
Unfortunately multi-argument applications must specify their lambda argument types (which we don't need to do for single-argument lambdas). But it's a small price to pay.
Before we would have had to write:
Here's the above example with three
IO<int>computations:Monad>>>
MonadoperatorsMonad action
The next operator to gain additional features is the
>>operator. Previously, I only used it for monad-action operations and couldn't fully generalise it. It has now been fully generalised to allow the sequencing of any two monad types.This forces the first computation to run before running the second one and returning its value.
Monad bind
The right-hand operand of
>>can also be a bind-delegate of the form:Func<A, K<M, B>>, i.e, monad-bind.Here's the
readLineexample from earlier, but we're using theIOreturning version ofparseInt.We can be even more concise in this instance:
Ultimately, this is equivalent to:
Choice>>>
ChoiceoperatorsChoice enables the
|operator where the left-hand side isK<F, A>and the right-hand side is alsoK<F, A>. The idea being is that if the left-hand side fails then the right-hand side is run.This is good for providing default values when one operation fails or standardised error messages.
In the previous example we can add a default error message in case the parse fails. First, let's create an
Errortype. This would usually be astatic readonlyfield somewhere in your app:Then we can catch the error and provide a default message:
That will yield a
"expected a number"exception if the parse fails.|can also be used whenPure<A>is the right-hand operand and theFtype is also anApplicative(allowing use to callF.Pure):This allows for sensible default values when the parse fails.
Fallible>>>
FallibleoperatorsFallibleshares the same|operator asChoicebut the right-hand operand can only be: aCatchM<E, F, A>struct (created by the@catchfunctions) orFail<E>created by thePrelude.Failfunction when working with theFallible<E, F>trait. Sdditionally the right-hand side can be theErrortype for theFallible<F>trait.So, in the previous example we don't need to use
IO.fail, we can use theErrorvalue directly, becauseIOsupports theFallible<IO>trait:Final>>>
FinaloperatorsThe final trait allows for an operation to be run regardless of the success (or not) of the left-hand side.
For example, if we create an IO operation to write
"THE END"to the console:We can then use the
|operator again to have afinallyoperation.SemigroupandSemigroupKSemigroup supports the
+operator for theSemigroup.CombineandSemigroupK.Combineassociative binary operations. Usually used for concatenation or summing.Bespoke downcast operators
The
.As()downcast extension method often requires wrapping an expression block with parentheses, which can be a bit ugly.I've started implementing the prefix
+operator for.As(). The beauty of it is it works quite elegantly with LINQ expressions and the like. For example, in theForkCancelExamplein theEffectsExamplessample. You can see how this:Becomes this:
It just instantly reduces the clutter and make the expression easier to read.
Status
All of the operators will automatically work for any types that implement the traits listed above. But the hand-coded ones that return concrete types like
IO<A>instead ofK<IO, A>have so far only been implemented for:IO<A>Eff<A>Eff<RT, A>I will do the rest of the types over the next week or so. I just wanted to get an initial version up, so you can all have a play with it and to get any feedback you may have.
v5.0.0-beta-55: Discriminated unions refactor + Coproducts [BREAKING CHANGES]This is quite a big change. I started writing it a few months back, then I moved house, and that disappeared several months of my life - all for very good reasons, so I'm not complaining! However, be aware, that I had a major context-switch halfway through this change. So, just be a little wary with this upgrade, there may be some inconsistencies here and there - especially as it was very much focusing on consistency!
There are also breaking changes (discussed in the next section). I don't want to be doing breaking changes at this stage of the beta, but there are good reasons, which I'll cover in the relevant sections.
IEnumerablesupport removed fromOption,Either,Fin, andValidation(BREAKING CHANGES)ThesetypeChronicleTtypeCoproductCons<F>Coproduct<F>CoproductK<F>BifunctortraitBimonadtraitCoreadabletraitSemigroupInstance<A>MonoidInstance<A>Discriminated union changes (BREAKING CHANGES)
I have changed the discriminated-union types (
Either,Validation, ...) to embed the case-types within the generic base-type instead of a non-generic module-type and changed the constructor functions to be embedded within the non-generic module-type instead of the generic base-type.So, previously a type like
Either<L, R>would be defined like so:Now
Either<L, R>is defined like this:So, the case-type
Either.Right<L, R>becomesEither<L, R>.Rightand the constructor-functionEither<L, R>.Right(A value)becomesEither.Right<L, R>(R value). In some circumstances, this makes the constructor functions slightly easier to use due to better generics inference.All sum-types in language-ext have been changed to maintain consistency across the library and hopefully will mean much less upheaval when C# unions turn up. Migration is pretty mechanical too.
Lifting (BREAKING CHANGES)
One benefit of moving the constructor-functions, outside of the type that it is constructing, is that we can put constraints on the functions that aren't on the type itself. This has tangible benefits for types that lift other types (like transformers).
And so, to try and build a consistent approach to construction, I've decided to move all lifting functions (which are just constructor functions with addtional logic) out of the type they are lifting and into the static module.
So, where before this was possible:
We can now only use the module version:
IEnumerablesupport removed fromOption,Either,Fin, andValidation(BREAKING CHANGES)Extension methods without traits are unfortunately poison to our more principled trait-based approach. And so, I need to not be inheriting
IEnumerableor other .NET types that have a billion extension methods hanging off.Option<A>,Either<L, R>,Fin<A>, andValidation<F, A>no longer supportIEnumerable. You can use them withIEnumerableby callingvalue.AsEnumerable().These<A, B>typeThis is a brand new type: the
Thesetype is a bit likeEitherexcept it has three states rather than two. The states are:This(A)- means "I have an alternative value" (likeLeftforEither)That(B)- means "I have a success value" (likeRightforEither)Both(A, B)- means we have a 'failure' value (A) but it's non-fatal as we also have a 'success' value (B), this allows for an alternative ('failure') value to be captured whilst also continuing a monadic operation. The way to think of this is like a warning in a compiler: we have a failure value, but we can continue.ChronicleT<Ch, M, A>typeChronicleTis a new transformer type that wraps up theThesetype (so, this is likeEitherTtoEither). It has a few methods accessible via either thePrelude, theChronicleTmodule, or theChronicleT<Ch, M>type. To reduce the amount of generics needed use:ChronicleT<Ch, M>:ChronicleT<Ch, M>.dictate(A)That(A)into the transformer (the success value).ChronicleT<Ch, M>.confess(Ch)This(A)into the transformer (the failure value).ChronicleT<Ch, M>.chronicle(Ch, A)Both(Ch, A)into the transformer (the success and falure values)ChronicleT.memento(ChronicleT<Ch, M, A>)ma.Memento()Eitherdual-state.ChronicleT.absolve(A, ChronicleT<Ch, M, A>)ma.Absolve(A)Avalue to use (to use if the structure doesn't already contain anAvalue).ChronicleT.condemn(ChronicleT<Ch, M, A>)ma.Codemn()Both(Ch, A)to aThis(Ch).Amongst other functionality.
CoproductCons,Coproduct, andCoproductKtraitsOne thing that has been building for a while is the need to standardise coproduct types. With the new
TheseandChronicleTtypes, we have two more coproducts. Coproducts are the dual of products. Product types are records and tuples. For example, a 2-tuple is (A * B) which means the number of values in the set ofAmultiplied by the number of values in the set ofB.So, a tuple
(bool, A)would be2 * A. A tuple(bool, uint)would be2 * UInt32.MaxValue. It represents the total possible combinations of values that can be stored in the type.Coproduct types are sum-types (we don't multiply, we add). The classic sum-type is
Either<L, R>, which meansL + R. So,Either<bool, A>would be2 + Apossible values.We now have a number of sum-types (coproducts), so standardising traits to work with all coproducts makes sense:
CoproductCons<F>CoproductConsallows construction of the the coproductF. We take theLeftandRightparlance fromEither, but it can construct any typeF<A, B>. This is likeApplicative.Purewhich allows us to construct a new applicative structureF<A>, but withCoproductConswe can represent alternative values too. This allows for standardising certain functionality where an alternative value is needed.Coproduct<F>Coproduct<F>brings in the opportunity to pattern-match on the coproduct structure so that we can work with theAor theBtype. Note that it inheritsCoproductCons<F>.Another thing to note is that we depend on the
K<F, A, B>type rather than theK<F, A>type. So if you're building your own coproduct types, then you need to derive your instance type fromK<F, A, B>andK<F, B>. SeeEither<L, R>for an example:That also means you need two trait-implementation classes: one for the
K<F, B>types and one for theK<F, A, B>types.So,
Eitherhas:Which are the trait-implementations that support
K<Either<L>, R>(where theLis 'baked in'). And...Which are the trait-implementations that support
K<Either, L, R>.CoproductK<F>One thing to note with
Coproduct<F>is that the return type from theMatchmethods are non-lifted types (likeA,B, andC). Certain coproduct types (like the transformer coproduct:EitherT) must be evaluated before we can extract the inner value and that can't always happen. So, the best thing we can do is re-lift the resultingA,B, orCvalues intoK<F, A, A>,K<F, A, B>, orK<F, A, C>, because we know we can create those.This is what that variant
CoproductK<F>is for, it's the same asCoproduct<F>but we always stays lifted in the results.Bifunctor trait
FirstrenamedMapFirstSecondrenamedMapSecondBimonadtraitThrough
BindFirstandBindSecondwe can now do monadic bind behaviour on boths sides of a coproduct.CoreadabletraitA restriction on the original
Readabletrait can be lifted by also providing aCoreadableimplementation for your types. The originalReadabletrait has a method calledLocal:It maps the readable 'environment' value. But, it can't (in generalised form), map to a different environment type. And so, that's left for concrete implementations in
ReaderTand the like.With
Coreadablethat limitation goes away:Experimental features
One of the limitations of the trait approach in language-ext is that we can have only one trait-implementation class (well, one for
K<F, A>and one forK<F, A, B>if we're creating product or coproduct types). But this limitation means certain traits, which should be ad-hoc, are left un-implemented because we would over-constrain the type if we used them in a non-ad-hoc way.For example, on
Validation<F, A>theFwas constrained toMonoid<F>. This allows for the aggregation of multiple failure values when using the applicative functionality and for a default 'empty' state forValidation<F, A>.Empty(). However, that stops us from implementingBifunctorandBimonad(which both map theFvalue to a new type that hasn't got theMonoidconstraint), limiting the extent of generic functionality that can be implemented.So, in this release I have some experimental features that loosen the trait constraints but tighten them elsewhere. For example,
Validation<F, A>doesn't have a constraint ofMonoid<F>on its type any more. But it does onValidation.Success<F, A>(value)andValidation.Fail<F, A>(error)- so the constructors act as aMonoid-trait gatekeeper, but the type itself doesn't.This behaviour extends a little further for types that aren't pure data-types, but are computations, like
ValidationT:This is what
ValidationTlooked like before:Note the
Monoid<F>constraint.And this is what it looks like now:
The
Monoid<F>constraint has been removed and now theK<M, Validation<F, A>> runValidationfield has been wrapped with aFuncthat provides aMonoidInstance<F>. This works like a reader monad and means the instance can be provided when the monad-transformer isRun.For this experiment I have added
SemigroupInstance<A>andMonoidInstance<A>...SemigroupInstance<A>SemigroupInstanceprovides aCombinefunction forA:The
Semigroup<A>trait has also been extended to give access to the instance value:MonoidInstance<A>MonoidInstanceprovides aCombinefunction (by inheritingSemigroupInstance<A>) and anEmptyproperty forA:As with
Semigroup<A>, we also have access to aMonoidInstance<A>value from theMonoid<A>trait:Experimental features conclusion
I'm not 100% happy with this feature. Ad-hoc polymorphism in this sense is effectively runtime resolution rather than compile-time. That opens up risks for unexpected runtime errors. They're unlikely to cause systemic issues, but still, compile-time is better.
However, this approach allows certain types to have decent fallback behaviour if a type hasn't implemented a required trait. We can also limit the constraints to the functions that need them.
Previously
Validation<F, A>was a monoid because of one function:Validation<F, A>.Empty(), and it was a semigroup because ofApplyandCombine. It would be preferable if we didn't have to constrain everything that touchesValidation<F, A>if we're not usingEmpty,Apply, andCombine. This I feel is a reasonably strong argument in favour of the approach. However, I think I'd still like to minimise the ad-hoc instance resolution.Update on version 5 release
With the .NET 10 release being imminent, I feel it's probably a good idea to align the release of language-ext version 5 with the .NET 10 release. I do want to access the 'extension operators' feature and fix up the operator inference before I do the language-ext RTM, but my thinking is to release within a month of .NET 10 RTM.
So, watch this space...
v5RTM coming soon!v5.0.0-beta-54: Refining the Maybe.MonadIO conceptA previous idea to split the
MonadIOtrait into two traits:Traits.MonadIOandMaybe.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
MonadIOtrait:MonadIO.LiftIO)MonadIO.ToIOandMonadIO.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
MonadIOcomes from leveragingToIO(for example,Repeat,Fork,Local,Await,Bracket, etc.) -- and so ifToIOisn'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
LiftIOandToIO, which have default-implementations (that throw) inMaybe.MonadIO<M>, don't have their overridden implementations enforced when someone implementsMonadIO<M>. We can just leaveLiftIOandToIOon their defaults, which means inheriting fromMonadIO<M>has no implementation guarantees.Solution
MonadIO(andMaybe.MonadIO) into distinct traits:MonadIOandMaybe.MonadIOfor lifting functionality (LiftIO)MonadUnliftIOandMaybe.MonadUnliftIOfor unlifting functionality (ToIOandMapIO)StateTandOptionT) then we only implementMonadIOMonadIOandMonadUnliftIO.MonadIOandMonadUnliftIO(the non-Maybe versions) we makeabstractthe methods that previously had defaultvirtual(exception throwing) implementations.Maybe.MonadIOandMaybe.MonadUnliftIOhave the*Maybesuffix (soLiftIOMaybe,ToIOMaybe, etc.)Maybevariants, but in the code it's declarative, we can see it might not work.MonadIOandMonadUnliftIO(the non-Maybe versions) we can overrideLiftIOMaybe,ToIOMaybe, andMapIOMaybeand get them to invoke the bespokeLiftIO,ToIO, andMapIOfromMonadIOandMonadUnliftIO.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
MonadIOandMonadUnliftIO, not theMaybeversions. So, it feels more 'intentional'.For example, this will work, because
ReaderTsupports lifting and unlifting because it implementsMonadUnliftIOWhereas this won't compile, because
StateTcan only support lifting (by implementingMonadIO):If you tried to implementing
MonadUnliftIOforStateTyou 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 SourceTIObservablecan now be lifted intoSourceandSourceTtypes (viaSource.lift,SourceT.lift, andSourceT.liftM).SourceorSourceTis now supports lifting of the following types:IObservableIEnumerableIAsyncEnumerableSystem.Threading.Channels.ChannelAnd, because both
SourceandSourceTcan be converted toProducerandProducerT(viaToProducerandToProducerT), all of the above types can therefore also be used in Pipes.v5.0.0-beta-51: LanguageExt.Streaming + MonadIO + DerivingFeatures:
SourceSourceTSinkSinkTConduitConduitTMonadIODerivingNew streaming library
A seemingly innocuous bug in the
StreamTtype 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.Streamingand theLanguageExt.Pipeslibrary has been deprecated.This is the structure of the
Streaminglibrary:Transducers are back
Transducers were going to be the big feature of
v5before 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-addedTransducerand 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 fromTransducerandTransducerM.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
Streaminglibrary 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
Pipessystem. The types in thePipessystem 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
MonadIOtrait 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
IObservablestreams.They yield values and (under certain circumstances) accept inputs.
SourceandSourceTyield values synchronously or asynchronously depending on their construction. Can support multiple readers.SinkandSinkTreceives values and propagates them through the channel they're attached to. Can support multiple writers.ConduitandConduitTprovides 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.SourceSource<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 theReduceor
ReduceAsyncvariants. These takeReducerdelegates 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
SubscribeinIObservablestreams, but are more principled because they return a value (which we can leverageto carry state for the duration of the stream).
Sourcealso 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.SourceTSourceT<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
staticmethods on theSourceTtype 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.Channelof effectsSourceT.liftM(IEnumerable<IO<A>> effects)lifts anIEnumerableof effectsSourceT.liftM(IAsyncEnumerable<IO<A>> effects)lifts anIAsyncEnumerableof effectsSourceTalso supports the same built-in convenience reducers asSource(Last,Iter,Collect).SinkSink<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 newSinkwith the manipulation 'built-in'.SinkTSinkT<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 newSinkTwith the manipulation 'built-in'.SinkTis also a transformer that lifts types ofK<M, A>.ConduitConduit<A, B>can be pictured as so:Ais posted to theConduit(viaPost)Transducer, mapping theAvalue toX(an internal type you can't see)Xvalue is then stored in the conduit's internal buffer (aSystem.Threading.Channels.Channel)Reducewill force the consumption of the values in the bufferXthrough the outputTransducerSo the input and output transducers allow for pre and post-processing of values as they flow through the conduit.
Conduitis aCoFunctor, callComapto manipulate the pre-processing transducer.Conduitis also aFunctor, callMapto manipulate the post-processing transducer. There are other non-trait, but common behaviours, likeFoldWhile,Filter,Skip,Take, etc.ConduitTConduitT<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)Reducewill force the consumption of the values in the bufferK<M, A>through the outputTransducerMSo the input and output transducers allow for pre and post-processing of values as they flow through the conduit.
ConduitTis aCoFunctor, callComapto manipulate the pre-processing transducer.Conduitis also aFunctor, callMapto 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
Pipessystem, 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
Pipescomponentslike
ProducerTandConsumerT.ConduitandConduitTsupportToProducer,ToProducerT,ToConsumer, andToConsumerT.SinkandSinkTsupportsToConsumer, andToConsumerT.SourceandSourceTsupportsToProducer, 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
ConduitTinternally to collect the values and to merge them into a singleProducerT.MonadIOBased on this discuission I have refactored
Monad,MonadIO, and created a newMaybe.MonadIO. This achieves the aims of the makingMonadIOa useful trait and constraint. The one difference between the proposal and my implementation is that I didn't makeMonadTinheritMonadIO.Any monad-transformer must add its own
MonadIOconstraint if allowsIOto 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.DerivingAnybody who's used Haskell knows the
derivingkeyword and its ability to provide trait-implementations automatically (for traits likeFunctorand 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
CardGamesample. TheGametrait-implementation looks like this:The only thing that needs implementing is the
TransformandCoTransformmethods. They simply unpack the underlying implementation or repack it.Deriving.Monadsimply implementsMonad<M>in terms ofTransformandCoTransform, 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
v5release. 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
IOmonad 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/awaitmachinery. The benefit of this is that we have noasync/awaitoverhead if there's no asynchronicity and only use it when we need it.But... the initial synchronous state-machine used a
try/finallyblock that was used to tidy up the internally allocatedEnvIO(and therefore any acquired resources). This is problematic when switching from `syncConfiguration
📅 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.