Skip to content

Breaking change: Case matching

Compare
Choose a tag to compare
@louthy louthy released this 05 Nov 13:07
· 1039 commits to main since this release

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 of Left and Right, and so if both cases are the same type, it's impossible to discriminate. If you need this, then the classic Match method should be used.

Collection types all have 3 case states:

  • Empty - will return null
  • Count == 1 will return A
  • 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 because Seq 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.