language-ext v3.5.24-beta Release Notes
Release Date: 2020-11-05 // over 4 years ago-
The
Case
feature of the collection and union-types has changed, previously it would wrap up the state of the collection or union type into something that could be pattern-matched with C#'s newswitch
expression. i.e.var result = option.Case switch { SomeCase\<A\> (var x) =\> x, NoneCase\<A\> \_=\> 0 }
The case wrappers have now gone, and the raw underlying value is returned:
var result = option.Case switch { int x =\> x, \_=\> 0 };
The first form has an allocation overhead, because the case-types, like
SomeCase
needed allocating each time. The new version has an allocation overhead only for value-types, as they are boxed. The classic way of matching, withMatch(Some: x => ..., None: () => ...)
also has to allocate the lambdas, so there's a potential saving here by using this form of matching.This also plays nice with the
is
expression:var result = option.Case is string name? $"Hello, {name}": "Hello stranger";
There are a couple of downsides, but but I think they're worth it:
object
is the top-type for all types in C#, so you won't get compiler errors if you match with something completely incompatible with the bound value- For types like
Either
you lose the discriminator ofLeft
andRight
, and so if both cases are the same type, it's impossible to discriminate. If you need this, then the classicMatch
method should be used.
Collection types all have 3 case states:
- Empty - will return
null
Count == 1
will returnA
Count > 1
will return(A Head, Seq<A> Tail)
For example:
static int Sum(Seq\<int\> values) =\>values.Case switch { null=\> 0, int x=\> x, (int x, Seq\<int\> xs) =\> x + Sum(xs), };
NOTE: The tail of all collection types becomes
Seq<A>
, this is becauseSeq
is much more efficient at walking collections, and so all collection types are wrapped in a lazy-Seq. Without this, the tail would be rebuilt (reallocated) on every match; for recursive functions like the one above, that would be very expensive.
Previous changes from v3.4.14-beta
-
An ongoing thorn in my side has been the behaviour of
Traverse
andSequence
for certain pairs of monadic types (when nested). These issues document some of the problems:The
Traverse
andSequence
functions were previously auto-generated by a T4 template, because for 25 monads that's25 * 25 * 2 = 1250
functions to write. In practice it's a bit less than that, because not all nested monads should have aTraverse
andSequence
function, but it is in the many hundreds of functions.Because the same issue kept popping up I decided to bite the bullet and write them all by hand. This has a number of benefits:
- ๐ The odd rules of various monads when paired can have bespoke code that makes more sense than any auto-generated T4 template could ever build. This fixes the bugs that keep being reported and removes the surprising nature of
Traverse
andSequence
working most of the time, but not in all cases. - ๐ I'm able to hand-optimise each function based on what's most efficient for the monad pairing. This is especially powerful for working with
Traverse
andSequence
on list/sequence types. The generic T4 code-gen had to create singleton sequences and the concat them, which was super inefficient and could cause stack overflows. Often now I can pre-allocate an array and use a much faster imperative implementation with sequential memory access. Where possible I've tried to avoid nesting lambdas, again in the quest for performance but also to reduce the amount of GC objects created. I expect a major performance boost from these changes. - The lazy stream types
Seq
andIEnumerable
when paired withasync
types likeTask
,OptionAsync
, etc. can now have bespoke behaviour to better handle the concurrency requirements (These types now haveTraverseSerial
andSequenceSerial
which process tasks in a sequence one-at-a-time, andTraverseParallel
andSequenceParallel
which processes tasks in a sequence concurrently with a window of running tasks - that means it's possible to stop theTraverse
orSequence
operation from thrashing the scheduler.
Help
๐ Those are all lovely things, but the problem with writing several hundred functions manually is that there's gonna be bugs in there, especially as I've implemented them in the most imperative way I can to get the max performance out of them.
๐ I have just spent the past three days writing these functions, and frankly, it was pretty soul destroying experience - the idea of writing several thousand unit tests fills me with dread; and so if any of you lovely people would like to jump in and help build some unit tests then I would be eternally grateful.
Sharing the load on this one would make sense. If you've never contributed to an open-source project before then this is a really good place to start!
I have...
- ๐ Released the updates in
3.4.14-beta
- so if you have unit tests that useTraverse
andSequence
then any feedback on the stability of your tests would be really helpful. - โ Created a github project for managing the cards of each file that needs unit tests. It's the first time using this, so not sure of its capabilities yet, but it would be great to assign a card to someone so work doesn't end up being doubled up.
- The code is in the hand-written-traverse branch.
- The folder with all the functions is transformers/traverse
Things to know
Traverse
andSequence
take a nested monadic type of the formMonadOuter<MonadInner<A>>
and flips it so the result isMonadInner<MonadOuter<A>>
- If the outer-type is in a fail state then usually the inner value's fail state is returned. i.e.
Try<Option<A>>
would returnOption<Try<A>>.None
if the outerTry
was in aFail
state. - If the inner-type is in a fail state then usually that short-cuts any operation. For example
Seq<Option<A>>
would return anOption<Seq<A>>.None
if any of theOptions
in theSeq
wereNone
. - ๐ป Where possible I've tried to rescue a fail value where the old system returned
Bottom
. For example:Either<Error, Try<A>>
. The new system now knows that the language-extError
type contains anException
and can therefore be used when constructingTry<Either<Error, A>>
- ๐ All async pairings are eagerly consumed, even when using
Seq
orIEnumerable
.Seq
andIEnumerable
do have windows for throttling the consumption though. - ๐
Option
combined with other types that have an error value (likeOption<Try<A>>
,Option<Either<L, R>>
, etc.) will putNone
into the resulting type (Try<Option<A>>(None)
,Either<L, Option<A>>(None)
if the outer type isNone
- this is because there is no error value to construct anException
orL
value - and so the only option is to either returnBottom
or a success value withNone
in it, which I think is slightly more useful. This behaviour is different from the old system. This decision is up for debate, and I'm happy to have it - the choices are: remove the pairing altogether (so there is noTraverse
orSequence
for those types) or returnNone
as described above
โ Obviously, it helps if you understand this code, what it does and how it should work. I'll make some initial tests over the next few days as guidance.
- ๐ The odd rules of various monads when paired can have bespoke code that makes more sense than any auto-generated T4 template could ever build. This fixes the bugs that keep being reported and removes the surprising nature of