Breaking change: Case matching
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 new switch
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, with Match(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.