Releases: louthy/language-ext
IO and effects refactoring
In the last release I wrote this:
"Because the traits are all interfaces we can't use
operator |
for error handling"
I realised that because @catch
creates a temporary struct
(the various Catch*
record structs) that I could attach operator |
to those types and make @catch
work for K<F, A>
types that are Fallible<F>
or Fallible<E, F>
.
This took me down a massive rabbit hole! So this release has quite a few changes. If you're using
v5
then you'll need to pay attention. And, if you're using runtimes, you'll really need to pay attention!
@catch
As, many of you know, in v4
we can @catch
errors raised in the Eff<RT, A>
and Eff<A>
types by using the |
operator like a coalescing operator. For example:
public static Eff<RT, Unit> main =>
from _1 in timeout(60 * seconds, longRunning)
| @catch(Errors.TimedOut, unit)
from _2 in Console<Eff<RT>, RT>.writeLine("done")
select unit;
This imposes a time-limit on the longRunning
operation, which throws a TimedOut
error if it doesn't finish in time. It then catches the timeout and continues safely by returning a default value of unit
.
There were a number of types that @catch
(depending on the overload) could create:
CatchValue<A>
- for returning default values (as above)CatchValue<E, A>
- for returning default values (with generic error type)CatchError
- for returning an alternative errorCatchError<E>
- for returning an alternative error (with generic error type)CatchIO<A>
- for returning and lifting anIO<A>
as the resultCatchIO<E, A>
- for returning and lifting anIO<A>
as the result (with generic error type)CatchM<M, A>
- for returning and lifting anK<M, A>
as the resultCatchM<E, M, A>
- for returning and lifting anK<M, A>
as the result (with generic error type)
Each one carries a predicate function and an action function. If the predicate returns true
for the error raised then the action is run, otherwise the result it left alone. This means a chain of | catch(...)
operations can effectively pattern match the errors raised.
Most importantly: the arguments to
@catch
can make the inference of the generic parameters automatic, so we don't have to manually write@catch<Error, Eff, int>(...)
-- this makescatch
usable.
Back to the idea that we have a Fallible<E, F>
(and Fallible<F>
which is equivalent to Fallible<Error, F>
). Because, operator declarations can't have generic parameters, all generic parameters must come from the type.
To be able to leverage the Fallible<E, F>
trait then we need F
(the trait type), E
(the error type), and A
(the bound value type):
public interface Fallible<E, F>
{
public static abstract K<F, A> Catch<A>(
K<F, A> fa,
Func<E, bool> Predicate,
Func<E, K<F, A>> Fail);
...
}
Only one of the Catch*
record structs has all of those generics:
CatchM<E, M, A>
- for returning and lifting anK<M, A>
as the result (with generic error type)
So, that's the only type that can support an operator |
that can work with Fallible<E, M>
:
public readonly record struct CatchM<E, M, A>(Func<E, bool> Match, Func<E, K<M, A>> Value)
where M : Fallible<E, M>
{
public static K<M, A> operator |(K<M, A> lhs, CatchM<E, M, A> rhs) =>
lhs.Catch(rhs.Match, rhs.Value);
}
So, I had a couple of options:
- Add support only to
CatchM
and leave the otherCatch*
types as non-Fallible supporting - Remove all of the other
Catch*
types that can't support Fallible
Option 1 would mean that some usages of @catch
would work with Eff<A>
but not K<Eff, A>
. This felt unsatisfactory.
Option 2 would mean that some of the convenience @catch
overrides would have to be removed. So, you couldn't write this anymore:
@catch(Errors.TimedOut, unit)
You'd have to write (one of):
@catch(Errors.TimedOut, SuccessEff(unit))
@catch(Errors.TimedOut, pure<Eff, Unit>(unit))
@catch(Errors.TimedOut, unitEff) // unitEff is a static readonly of SuccessEff
Option 2 is the option I've gone with. The reasons for this are primarily for consistency between the concrete types (Eff<A>
) and their abstract pairs (K<Eff, A>
), but also...
Every single Fallible
type gets to use @catch
!
So, previously, @catch
only worked for Eff<RT, A>
, Eff<A>
, and IO<A>
. It now works for:
IO<A>
Eff<RT, A>
Eff<A>
Either<L, R>
EitherT<L, M, R>
Fin<A>
FinT<M, A>
- more on this laterOption<A>
OptionT<M, A>
Try<A>
TryT<A>
Validation<F, A>
ValidationT<F, M, A>
So now all Fallible
types get to use @catch
and they all get to use the same set (well, some are specifically for the Error
type, like @expected
and @exceptional
, but other than that they're all the same).
Things to note about this change:
- Because
@catch
is now entirely generic and based aroundFallible
types, the|
operator can only returnK<M, A>
, so you may need to use.As()
if you need to get back to the concrete type. - For catch-all situations, it's better to not use
@catch
at all, unless you need access to the error value.
MonadIO
refactor
The generalisation of catching any errors from Fallible
led to me doing some refactoring of the Eff<RT, A>
and Eff<A>
types. I realised not all errors were being caught. It appeared to be to do with how the IO
monad was lifted into the Eff
types. In the Monad<M>
trait was a function: WithRunInIO
which is directly taken from the equivalent function in Haskell's IO.Unlift
package.
I decided that was too complicated to use. Every time I used it, it was turning my head inside out, and if it's like that for me then it's probably unusable for others who are not fully aware of unlifting and what it's about. So, I removed it, and UnliftIO
(which depended on it).
I have now moved all lifting and unlifting functions to MonadIO
:
public interface MonadIO<M>
where M : MonadIO<M>, Monad<M>
{
public static virtual K<M, A> LiftIO<A>(IO<A> ma) =>
throw new ExceptionalException(Errors.LiftIONotSupported);
public static virtual K<M, A> LiftIO<A>(K<IO, A> ma) =>
M.LiftIO(ma.As());
public static virtual K<M, IO<A>> ToIO<A>(K<M, A> ma) =>
throw new ExceptionalException(Errors.UnliftIONotSupported);
public static virtual K<M, B> MapIO<A, B>(K<M, A> ma, Func<IO<A>, IO<B>> f) =>
M.ToIO(ma).Bind(io => M.LiftIO(f(io)));
}
Monad<M>
inheritsMonadIO<M>
, which isn't how it should be, but because of the limitations of C#'s type system we have all monads expose theMonadIO
functionality (otherwise monad-transformers won't work). I'm still thinking through alternative approaches, but I'm a little stumped at the moment. So, for now, there are default implementations forLiftIO
andToIO
that throw exceptions. You only implement them if your type supports IO.
LiftIO
as most will know, will lift anIO<A>
into your monad-transformer.ToIO
is the opposite and will unpack the monad-transformer until it gets to theIO
monad and will then return that as the bound value.
For example, this is the implementation for ReaderT
:
public static ToIO<A>(K<ReaderT<Env, M>, A> ma) =>
new ReaderT<Env, M, IO<A>>(env => ma.As().runReader(env).ToIO());
So, we run the reader function with the env
environment-value, it will return a K<M, A>
which we then call ToIO()
on to pass it down the transformer stack. Eventually it reaches the IO
monad that just returns itself. This means we run the outer shell of the stack and not the inner IO
.
That allows methods like MapIO
to operate on the IO<A>
monad, rather than the <A>
within it:
M.ToIO(ma).Bind(io => M.LiftIO(f(io)));
What does this mean?
- It means you can call
.MapIO(...)
on any monad that has anIO
monad within it (as long asToIO
has been implemented for the whole stack) - Once we can map the IO we can generalise all of the IO behaviours...
Generalised IO behaviours
The IO<A>
monad has many behaviours attached to it:
Local
- for creating a local cancellation environmentPost
- to make the IO computation run on theSynchronizationContext
that was captured at the start of the IO operationFork
- to make an IO computation run on its own threadAwait
- for awaiting a forked IO operation's completionTimeout
- to timeout an IO operation if it takes too longBracket
- to automatically track resource usage and clean it up when doneRepeat
,RepeatWhile
,RepeatUntil
- to repeat an IO operation until conditions cause the loop to endRetry
,RetryWhile
,RetryUntil
- to retry an IO operation until successful or conditions cause the loop to endFold
,FoldWhile
,FoldUntil
- to repeatedly run an IO operation and aggregating a result until conditions cause the loop to endZip
- the ability to run multiple IO effects in parallel and join them in a tuppled result.
Many of the above had multiple overrides, meaning a few thousand lines of code. But, then we put our IO
monad inside monad-transformers, or encapsulate them inside types like Eff<A>
and suddenly those functions above are not available to us at all. We can't get at the IO<A>
monad within to pass as arguments to the IO behaviours.
That's where MapIO
comes in. Any monadic type or transformer type that has implemented ToIO
(and has an IO<A>
monad encapsulated within) can now directly invoke ...
New trait: Fallible
In Haskell there's a trait called MonadFail
for monadic types to raise errors. It's not particularly effective as most tend to avoid it. I wanted to create a trait (for types that can fail) that's effective and could help standardise error handling.
That's the new Fallible<E, F>
and Fallible<F>
trait type...
Fallible
Traits that are fallible can fail (I'm quite smug about the name, I think it's pretty cool, haha)!
Fallible<E, F>
- can have a parameterised failure valueE
for structureF
(usually a functor, applicative, or monad)Fallible<F>
is equivalent toFallible<Error, F>
- which simplifies usage for the commonly usedError
type
Anything that is fallible must implement:
public static abstract K<F, A> Fail<A>(E error);
public static abstract K<F, A> Catch<A>(
K<F, A> fa,
Func<E, bool> predicate,
Func<E, K<F, A>> fail);
Fail
is for the raising of errorsCatch
can be used to catch an error if it matches apredicate
; and if so, it runs thefail
function to produce a new structure (which may also be failing, but could be used to rescue the operation and provide a sensible succeeding default).
Fallible
module
In the Fallible
module there are functions to raise failures:
public static class Fallible
{
public static K<F, A> fail<E, F, A>(E error)
where F : Fallible<E, F> =>
F.Fail<A>(error);
public static K<F, Unit> fail<E, F>(E error)
where F : Fallible<E, F> =>
F.Fail<Unit>(error);
public static K<F, A> error<F, A>(Error error)
where F : Fallible<Error, F> =>
F.Fail<A>(error);
public static K<F, Unit> error<F>(Error error)
where F : Fallible<Error, F> =>
F.Fail<Unit>(error);
}
fail
raises the parameterised error typeserror
raises theError
type
Because the traits are all interfaces we can't use operator |
for error handling (the operators can still be used for concrete types, like Eff<A>
, IO<A>
, etc.) -- and so there are now lots of Catch
extension methods for catching errors in Fallible
structures. You can view them here.
Prelude
The Prelude
now has:
public static K<F, A> pure<F, A>(A value)
where F : Applicative<F>;
public static K<F, A> fail<E, F, A>(E error)
where F : Fallible<E, F>;
public static K<F, Unit> fail<E, F>(E error)
where F : Fallible<E, F>;
public static K<F, A> error<F, A>(Error error)
where F : Fallible<F>;
public static K<F, Unit> error<F>(Error error)
where F : Fallible<F>;
So, for example, you can now construct any type (as long as it implements the Applicative
trait) using pure
:
var effect = pure<Eff, int>(100);
var option = pure<Option, string>("Hello");
var either = pure<Either<Error>, bool>(true);
And you can construct any type (as long as it implements the Fallible<E, F>
trait) using fail
or with error
(when Fallible<F>
):
var effect = error<Eff, int>(Errors.SequenceEmpty);
var trying = error<Try, int>(Errors.EndOfStream);
var option = fail<Unit, Option, string>(unit);
var either = fail<string, Either<string>, bool>("failed!");
Types that have been made Fallible
IO<A>
Eff<RT, A>
Eff<A>
Either<L, R>
EitherT<L, M, R>
Fin<A>
Option<A>
OptionT<M, A>
Try<A>
TryT<A>
Validation<F, A>
ValidationT<F, M, A>
Which means you can use .Catch(...)
on all of those types now. For example:
var res = error<Eff, int>(Errors.Cancelled)
.Catch(Errors.EndOfStream, _ => 0) // Catch a specific error and ignore with a default
.Catch(Errors.Closed, _ => pure<Eff, int>(-1)) // Run an alternative effect
.Catch(Errors.Cancelled, _ => IO.pure(-2)) // For monads that support IO, launch an IO operation
.Catch(e => Errors.ParseError(e.ToString())); // Catch-all mapping to another error
IO
changes
IO.Pure
has been renamedIO.pure
- Capital letters are used for constructor cases (like
Some
andNone
in a discriminated union) IO.Pure
doesn't create anIO<A>
type with aPure
case, it constructs a lambda that returns anA
- So, I'm going to change the construction functions where they're not really doing what they claim
- Capital letters are used for constructor cases (like
IO.Fail
has been renamedIO.fail
(see above)
Eff
running extensions
The various .Run*
methods for Eff<A>
and Eff<RT, A>
have been made into extension methods that work with K<Eff, A>
and K<Eff<RT>, A>
. That means if you end up with the more abstract representation of Eff
you can run it without calling .As()
first.
I'll be doing this for other types that are 'run'.
I have also tidied up some of the artefacts around the MinRT
runtime used by Eff<A>
. Because Eff<A>
is now backed by a transformer stack with IO<A>
as its inner monad, the MinRT
doesn't need to carry any IO environment any more, so I've removed it from MinRT
, making MinRT
into a completely empty struct. This removes some constraints from the Run*
extensions.
Prelude.ignoreF
and Functor.IgnoreF
The prelude function ignoreF
and equivalent extension method to Functor<F>
, IgnoreF
are the equivalent of calling .Map(_ => unit)
to ignore the bound-value of a structure and instead return unit
.
Transducers removed
I have removed the Transducers completely from v5
. They were originally going to be the building blocks of higher-kinds, but with the new trait-system I don't think they add enough value, and frankly I do not have the time to bring them through this v5
release process (which is already a mammoth one)! As much as I like transducers, I think we can do better with the traits system now.
Not needed traits removed
The following traits have been removed:
HasCancel
- This was used in theAff
monad and now isn't needed because theIO
monad has its own environment which carries the cancellation tokenHasFromError
- was used by the transducers, so not neededHasIO
- was used by theMinRT
runtime, which isn't needed anymoreHasSyncContextIO
- as above
New Sample
Those of you who are subscribed to my blog at paullouth.com will have seen the first newsletter this week. It took a while to get off the ground because I refused to use the terrible Mailgun integration in GhostCMS.
Instead I rolled my own, which I've been working on the past few days. So it was an opportunity to test out the effect system and trait system. I took it as far as it can go and the entire application is trait driven. Only when you invoke the application do you specify what monad and runtime to use.
This is the main operation for generating the newsletter and emailing it out to all of the members:
public static class Send<M, RT>
where RT :
Has<M, WebIO>,
Has<M, JsonIO>,
Has<M, FileIO>,
Has<M, EmailIO>,
Has<M, ConsoleIO>,
Has<M, EncodingIO>,
Has<M, DirectoryIO>,
Reads<M, RT, Config>,
Reads<M, RT, HttpClient>
where M :
Monad<M>,
Fallible<M>,
Stateful<M, RT>
{
public static K<M, Unit> newsletter =>
from posts in Posts<M, RT>.readLastFromApi(4)
from members in Members<M, RT>.readAll
from templates in Templates<M, RT>.loadDefault
from letter in Newsletter<M, RT>.make(posts, templates)
from _1 in Newsletter<M, RT>.save(letter)
from _2 in Display<M, RT>.showWhatsAboutToHappen(members)
from _3 in askUserToConfirmSend
from _4 in Email<M, RT>.sendToAll(members, letter)
from _5 in Display<M, RT>.confirmSent
select unit;
..
}
Note how the computation being run is entirely generic: M
. Which is constrained to be a Monad
, Fallible
, and Stateful
. The state is RT
, also generic, which is constrained to have various IO traits as well as a Config
and HttpClient
state. This can be run with any type that supports those traits. Completely generic and abstract from the underlying implementation.
Only when we we pass the generic argument to Send<>
do we get a concrete implementation:
var result = Send<Eff<Runtime>, Runtime>.newsletter.Run(runtime);
Here, we run the newsletter
operation with an Eff<Runtime>
monad. But, it could be with any monad we build.
Importantly, it works, so that's good :)
Source code is here . Any questions, ask in the comments below...
StateT bug fix + monadic conditionals
- Fixed: a bug in the
StateT
monad-transformer. One of theSelectMany
overloads wasn't propagating the state correctly. - Changed:
Prelude.local
that creates a local IO and resource environment renamed tolocalIO
to avoid conflicts withReaderT.local
andReader.local
- Added: general purpose
liftIO
inPrelude
- Added: variants of
when
andunless
that take aK<M, bool>
as the source of the flag. Means any monad that binds abool
can be used directly inwhen
andunless
, rather than having to lower it first. - Added: new monadic conditional:
iff
- works likewhen
andunless
, but has anelse
case.K<M, bool>
can be used directly also, meaning that if/then/else monadic expressions can be built without lowering. - Added: applicative
actions
to thePrelude
. Allows for chainingn
applicative actions, discarding their results, apart from the last one, which is returned
Fix for: use of custom sub-type errors in IO monads
This is a minor release to fix: issue 1340.
Thanks to @HernanFAR for raising the issue with concise repro steps 👍
LanguageExt v5 first beta
I'm now moving the v5
release from alpha to beta. Not because I'm feature complete, but because from my real-world testing of v5
(with my new startup project) it is much more stable than I expected. In fact I haven't hit any issues at all outside of missing functionality.
So, this is more of a 'soft launch beta', primarily for those who were perhaps not ready to use language-ext in alpha form but are more likely to in beta form.
Removed ResourceT transformer / integrated into IO
This release removes the ResourceT<M, A>
monad-transformer from language-ext and instead moves the functionality into the IO<A>
monad. ResourceT
required IO
to be in the transformer stack and so it really was adding complexity to a feature that's closely linked. This adds a tiny overhead to the IO
monad -- the IO monad already carried an environment through its computations, so this doesn't change much -- in the big scheme of things it's likely to bring performance benefits.
Some big improvements because of this:
use
andrelease
are now available in thePrelude
, which makes them easier to work with (no need for any manual generic arguments), everything is inferable from usage.- Forking an
IO
computation (launching it on a new thread) automatically creates a local resource environment for the fork and cleans it up when the forked operation is complete. - Repeating an
IO
computation (repeat(computation)
) - will automatically clean up any resources acquired byuse
in the computation (on each iteration). - Retrying an
IO
computation (retry(computation)
) - will automatically clean up any resources (acquired withuse
) when the computation fails, that mean retries don't accumulate resources unnecessarily. - New function
local(computation)
works as a 'superusing
' -- in that it will automatically clean up any resources acquired withuse
in the computation. This allows you to create local scopes where you just freely acquire resources and then have a clean-up happen automatically.- By the way, I am open to different names for this, as we already have
IO.local
for local cancellation contexts andReader.local
for local environments. I'm also open to changing the names of the others. Ideally any name would be a single word so it's easy on the eye. So, nothing likelocalResource
orcleanUp
.
- By the way, I am open to different names for this, as we already have
- New functions
bracket(Acq, Use, Err, Fin)
andbracket(Acq, Use, Fin)
- these are liketry
\catch
\finally
blocks for more explicit resource usage:Acq
- acquires the resourceUse
- uses the resourceErr
- is the catch blockFin
- is the finally block
All the usual caveats apply: this is an alpha, this isn't fully tested, use at your own risk.
New Try monad and updated TryT transformer
I've re-added a Try<A>
monad (I always intended to re-add it, just hadn't got around to it). And I've and reimplemented the TryT<M, A>
monad-transformer in terms of Try
(K<M, Try<A>>
), previously it was implemented in terms of Fin
(Func<K<M, Fin<A>>>
).
I have also:
- Added
Match
andIfFail
methods for pattern matching on the success/fail state. - Added
|
operator@catch
overrides to allow for easy error handling.
The IO
monad also has a .Try()
method that will run the IO
monad in a try/catch block returning IO<Fin<A>>
for more manual handling of IO errors.
Still needs some in-depth testing to make sure all exceptions are captured, but it's effectively feature complete.
Language-Ext 5.0 alpha-3
WARNING: THIS IS AN ALPHA RELEASE AND SHOULD BE CONSUMED WITH CARE! NOT FOR PRODUCTION.
Bug fixing and TODO resolving release, with some minor featurettes!
For those that don't know yet (and there's no reason to think you should, because I haven't announced it yet) -- the Pipes Effect
system now has the ability to lift any monad into its stack (previously it only allowed Aff
to be lifted). It is now a general monad transformer like ReaderT
, OptionT
, EitherT
, etc.
As, with all monad-transfomers, when you 'run' the transformer, it generates the lifted monad. You can think of this being like a mini-compiler that takes the monad stack and compiles down to the inner-most monad, which can then just be run as normal.
The problem for Pipes is that there's usually lots of recursion, repetition (using repeat
, retry
), or iteration (using yieldAll
, etc.). This is problematic when you don't know anything about the inner monad. The transformer can't run the inner monad, because it only has access to the Monad
interface (Bind
) and the inherited interfaces of Applicative
and Functor
(Apply
, Action
, Map
, and Pure
). So, doing iteration requires recursion, and recursion blows the stack in C#.
Previously Pipes were able to directly
Run
theAff
because the Pipe system knew it was working only withAff
. This allowed it to flatten the recursion.
Anyway, now Pipes has internal support for any Foldable
. That means yieldAll(...)
can take a sequence from any foldable (Range
, EnumerableM
, HashMap
, HashSet
, Lst
, Map
, Seq
, Either
, Option
, Validation
, Identity
, ... and any you write) and yield the values within the structure through the pipe. Functions like repeat(ma)
- which continually repeat an operation until it fails - have also been implemented internally as something that iterates over an infinite foldable.
This functionality has been enabled by adding a new method to the Applicative
trait: Actions
. You might know the existing Action(K<M, A> ma, K<M, B> mb)
method that runs the first applicative (ma
), ignores its result, and then runs the second applicative mb
, returning its result.
Actions
instead takes an IEnumerable<K<M, A>>
:
K<F, A> Actions<A>(IEnumerable<K<F, A>> fas)
It runs each applicative action and ignores its result, returning the result of the last item. That means a sequence of Proxy
values (Proxy
is the monad-transformer for pipes) can be mapped - the map will just run (using RunEffect
) the Proxy - producing a sequence of whatever the lifted inner-monad is for the Proxy
. This lazy sequence of monads can then be invoked by calling Actions
on it, which will lazily walk the sequence, evaluating the inner-monad one-by-one.
There is a default implementation, but it has the same lack of knowledge that Pipes had, so it should be overridden for computation based applicatives (that usually need invoking with without an argument). Here's the override for Eff<RT, A>
:
static K<Eff<RT>, A> Applicative<Eff<RT>>.Actions<A>(IEnumerable<K<Eff<RT>, A>> fas) =>
from s in getState<A>()
from r in Eff<RT, A>.Lift(
rt =>
{
Fin<A> rs = Errors.SequenceEmpty;
foreach (var kfa in fas)
{
var fa = kfa.As();
rs = fa.Run(rt, s.Resources, s.EnvIO);
if (rs.IsFail) return rs;
}
return rs;
})
select r;
You can see how:
- It's able to gather information, like the runtime, resources, and IO environment.
- It knows how to run itself, whereas the generic transformer can't.
- It can shortcut the operation when any effect fails.
And so, if you want to use your own monads with Pipes then you should implement Actions
.
There's still more to do with Pipes, but all of the examples in EffectsExamples
now work, which is a good sign!
WARNING: THIS IS AN ALPHA RELEASE AND SHOULD BE CONSUMED WITH CARE! NOT FOR PRODUCTION.
Language-Ext 5.0 alpha-2
WARNING: THIS IS AN ALPHA RELEASE AND SHOULD BE CONSUMED WITH CARE! NOT FOR PRODUCTION.
General updates
Free
monad doesn't needAlternative
trait: removed- All semigroup and monoid-like types have their
Append
operator renamed toCombine
. 'Combine' works semantically for more of the monoidal associative operations thanAppend
(which really only makes sense with collections). - Added new
SemigroupK
andMonoidK
-- these are like theSemigroup
andMonoid
traits except they work onK<M, A>
instead ofA
. These are almost identical toSemiAlternative
andAlternative
execept they don't require the underlying value to an anApplicative
. The idea here is thatSemigroupK
andMonoidK
would be used on types like collections that 'sum' when theCombine
operator is applied, whereasSemiAlternative
andAlternative
provide an alternative value when theCombine
operator is applied (coalescing). - Added missing
repeat
variants,retry
variants, andtimeout
for the IO monad - Added
IO.yieldFor(TimeSpan)
. This is likeTask.Delay
but for the IO monad. The war against async means that this does the thread-yielding internally, no need to call await. I figuredyieldFor
is more meaningful thanDelay
, it indicates that the thread is yielding, not simply blocking. - Added support for guards in the IO monad
- Initial pass at a continuation-monad transformer:
ContT<R, M, A>
-- just the raw type for now. HeadOrNone
,HeadOrInvalid
,HeadOrLeft
,LastOrNone
, etc. have been removed.Head
andLast
are nowOption
returning. This is a breaking change. Can be mitigated by either matching, casting, or invocation of.ValueUnsafe()
extension.- Improved
Range
type -- previously there were several types (IntegerRange
,CharRange
, etc.) -- now there's just one:Range<A>
. It leverages the new traits built into .NET (IComparisonOperators
,IAddtionOperators
, etc.) - Started adding support for more of the .NET traits in the collection types and various other places like
Foldable.Sum
,Foldable.Max
, etc.: (IComparisonOperators
,IAddtionOperators
,IAdditiveIdentity
etc.) -- these are Microsoft's ugly named versions of monoids etc.
War on Extension Methods
I'm rapidly coming to the conclusion that extension-methods are a terrible idea. Especially in a library like language-ext where I am trying to present a consistent set of interfaces to types that share common traits. It's just impossible to enforce consistency and relies on the human eye -- and that errs regularly!
The latest move toward using traits is really starting to help reduce the extension methods, or at least mean the extension methods are hanging off traits rather than individual instance-types.
One change that I have made recently is to change Foldable
to require implementation of FoldWhile
and FoldWhileBack
instead of Fold
and FoldBack
. This means that so many more default behaviours can hang off of Foldable
-- and most of them are optimal. For example, Exists
-- which can stop processing as soon as its predicate returns true
-- couldn't early-out before.
And so, the foldable trait is now growing to have a ton of functionality. Also nested foldables!
However, quite a lot of those methods, like Sum
, Count
, etc. also exist on IEnumerable
. And so, for a type like Seq
which derives from both IEnumerable
and K<Seq, A>
, there will be extension method resolution issues.
So, the choice is to provide extension methods for IEnumerable
(an ill defined type) or for Foldable
- a super featureful type with the opportunity for implementers to provide bespoke optimised overrides.
Really, the choice should be easy: extensions for Foldable are just better than extensions for IEnumerable
. So, I have done that. The downside is that this will be another breaking change (because the IEnumerable
extensions have been removed). The fix is to convert from IEnumerable<A>
to EnumerableM<A>
using .AsEnumerableM()
. EnumerableM<A>
supports Foldable
(and other traits).
Conclusion
So, I've been working to remove as many non-trait extension methods as I can -- and I will continue to do so leading up to the beta. This will bring consistency to the code-base, reduce the amount of code, and provide ample opportunities for bespoke optimisations. Just be aware that this is another fix-up job.
WARNING: THIS IS AN ALPHA RELEASE AND SHOULD BE CONSUMED WITH CARE! NOT FOR PRODUCTION.
Language-Ext 5.0 alpha-1
This release should only be consumed by those who are interested in the new features coming in the monster v5
release.
Just to give you an idea of the scale of this change:
- 193 commits
- 1,836 files changed
- 135,000 lines of code added (!)
- 415,000 lines of code deleted (!!)
It is a monster and should be treated with caution...
- It is not ready for production
- It is not feature complete
- The new features don't have unit tests yet and so are probably full of bugs
- I haven't yet dogfooded all the new functionality, so it may not seem as awesome as it will eventually become!
If you add it to a production project, you should only do so to see (potentially) how many breaking changes there are. I would not advise migrating a production code-base until I get close to the final release.
I am also not going to go into huge detail about the changes here, I will simply list them as headings. I will do a full set of release notes for the beta
release. You can however follow the series of articles I am writing to help you all prep for v5
-- it goes (and will go) into much more detail about the features.
New Features
- Higher-kinded traits
K<F, A>
- higher-kinds enabling interface- Includes:
- Defintions (interfaces listed below)
- Static modules (
Functor.map
,Alternative.or
,StateM.get
, ...) - Extension methods (
.Map
,.Or
,Bind
, etc.), - Extension methods that replace LanguageExt.Transformers (
BindT
,MapT
, etc. ), now fully generic. - Trait implementations for all Language-Ext types (
Option
,Either<L>
, etc.)
Functor<F>
Applicative<F>
Monad<M>
Foldable<F>
Traversable<T>
Alternative<F>
SemiAlternative<F>
Has<M, TRAIT>
Reads<M, OUTER_STATE, INNER_STATE>
Mutates<M, OUTER_STATE, INNER_STATE>
ReaderM<M, Env>
StateM<M, S>
WriterM<M, OUT>
MonadT<M, N>
- Monad transformersReaderT<Env, M, A>
WriterT<Out, M, A>
StateT<S, M, A>
IdentityT<M, A>
EitherT<L, M, R>
ValidationT<F, M, S>
OptionT<M, A>
TryT<M, A>
IdentityT<M, A>
ResourceT<M, A>
Free<F, A>
- Free monadsIO<A>
- new IO monad that is the base for all IOEff<RT, A>
monad rewritten to use monad-transformers (StateT<RT, ResourceT<IO>, A>
)Eff<RT, A>
doesn't needHasCancel
trait (or any trait)- Transducers
Pure
/Fail
monads- Lifting
- Improved guards, when, unless
- Nullable annotations - still WIP, mostly complete on Core)
- Collection initialisers
Breaking changes
- netstandard2.0 no longer supported (.NET 8.0+ only)
Seq1
made[Obsolete]
- 'Trait' types now use static interface methods
- The 'higher-kind' trait types have all been refactored
- The
Semigroup<A>
andMonoid<A>
types have been refactored - The static
TypeClass
class has been renamedTrait
Apply
extensions that use rawFunc
removed- Manually written
Sequence
extension methods have been removed - Manually written
Traverse
extension methods have been removed ToComparer
doesn't exist on theOrd<A>
trait any more- Renamed
LanguageExt.ClassInstances.Sum
Guard<E>
has becomeGuard<E, A>
UnitsOfMeasaure
namespace converted to a static classEither
doesn't supportIEnumerable<EitherData>
any moreEither
'bi' functions have their arguments flipped- Nullable (struct) extensions removed
- Support for
Tuple
andKeyValuePair
removed - Types removed outright
Some<A>
OptionNone
EitherUnsafe<L, R>
EitherLeft<L>
EitherRight<L>
Validation<MFail, Fail, A>
Try<A>
TryOption<A>
TryAsync<A>
TryOptionAsync<A>
Result<A>
OptionalResult<A>
- Async extensions for
Option<A>
ExceptionMatch
,ExceptionMatchAsync
,ExceptionMatchOptionalAsync
- Libraries removed outright
LanguageExt.SysX
LanguageExt.CodeGen
LanguageExt.Transformers