Show / Hide Table of Contents

Match Expressions

The second central idea is the match expression itself. It is represented by two classes: Match<TInput, TOutput> and Match<TInput>. The difference between them is that the former represents a match expression which yields a result, and the latter represents a match expression which doesn't yield a result (also known as match statement).

Using Match Expressions

Creating Match Expressions

A match expression can be created using the Create methods of the static class Match.

Adding Cases

The Match classes include Case methods which are used to add a pattern and a function which is executed if the match is successful. Match expressions are immutable - Case methods return new match expressions; they do not affect the ones on which they are called.

Case methods are generic - they also contain information about the pattern's transformation type. Match expressions can contain patterns of arbitrary transformation types without knowing about these types.

Executing Match Expressions

To execute a match expression, the ExecuteOn method is used. It takes the input value to match. There are two modes of execution in match expressions: strict and non-strict. The strict mode throws an exception if no matches were found, and the non-strict doesn't.

In the Match<TInput, TOutput> class the ExecuteOn method returns the result of the match or throws a MatchException if no successful match was found. Match<TInput, TOutput> also contains the ExecuteNonStrict method which executes the match expression in the non-strict mode. It returns MatchResult<TOutput> because the result might not be present.

In the Match<TInput> class the ExecuteOn method doesn't return anything, and also throws a MatchException if the match wasn't successful. This class also contains the ExecuteNonStrict method - it returns a boolean value which indicates whether the match was successful and doesn't throw an exception if it wasn't.

The ToFunction method and its variations are also available. They return a function which, when called, will execute the match expression.

Matching with Fall-through

C, C++ and, Java support fall-through in switch statements. So does this library, although it works differently here.

Fall-through must be explicitly enabled for cases and then explicitly enabled during execution. Both Match classes contain the ExecuteWithFallthrough method which takes fall-through behavior into account. ExecuteOn and ExecuteStrict ignore the fall-through behavior.

If a case has fall-through enabled, then the expression falls to the next successful match, unlike switch, which falls to the next case whether it's successful or not.

The Case methods are overloaded to accept a boolean value which indicates the fall-through behavior. If fall-through is enabled for a pattern, then the expression will continue searching for the next successful pattern. If it isn't, then the expression will stop at this pattern and not go any further.

Match.Create is also overloaded to take the default fall-through behavior.

Matching with fall-through is lazy i.e. it returns an IEnumerable and is only executed when this enumerable is enumerated. Because matching will fall-through is lazy, it doesn't have any modes of execution - the user must decide whether to throw an exception or not if there were no successful matches.

In the Match<TInput, TOutput> class the ExecuteWithFallthrough method returns an IEnumerable<TOutput> which can be used to get all successful match results.

In the Match<TInput> class there are no results, so the ExecuteWithFallthrough method returns an IEnumerable<object> which should be used simply to enumerate the match process itself. This is implemented so that matching with fall-through is also lazy in this class. The values of the resulting enumerable don't matter - in fact, they are always null, because match statements don't produce any results. What matters is the process of enumeration.

You can use the LINQ's Take method to limit the number of executed matches, or the Count method to execute it and get the number of successful matches.

The Matchmaker.Linq namespace contains the Enumerate extension method for IEnumerable<T> which enumerates it and ignores the result. You can use it if you just want to execute the match statement with fall-through.

Remember: matching with fall-through is lazy and is actually executed when the result is enumerated.

Here's a (somewhat convoluted) implementation of the famous fizz-buzz program which uses matching with fall-through:

using System.Linq;

using Matchmaker;
using Matchmaker.Linq;

using static Matchmaker.Patterns.Pattern;

// ...

IPattern<int, int> DivisibleBy(int n) =>
    CreatePattern<int>(input => input % n == 0);

var result = Enumerable.Range(0, 15)
    .Select(Match.Create<int, string>(fallthroughByDefault: true)
        .Case(DivisibleBy(3), _ => "Fizz")
        .Case(DivisibleBy(5), _ => "Buzz")
        .Case(Not(DivisibleBy(3).Or(DivisibleBy(5))), n => n.ToString())
        .ToFunctionWithFallthrough())
    .Select(items => items.Aggregate(String.Concat))
    .ToList();

// The result is ("FizzBuzz", "1", "2", "Fizz", "4", "Buzz", "Fizz", "7", "8", "Fizz", "Buzz", "11", "Fizz", "13", "14", "FizzBuzz");

Static Match Expressions

The Initialization Problem

One pain point of match expressions is that whenever a method which contains a match expression is executed, the match expression is initialized from scratch. Take a look at this example:

void DoStuff(int i) =>
    Match.Create<int, string>()
        .Case(...)
        .Case(...)
        .Case(...)
        .Case(...)
        .ExecuteOn(i);

The problem here is that if we call DoStuff 10,000 times, we will initialize the match expression 10,000 times as well, even though it's actually the same expression. Having just 4 cases may not seem like much, to the lag does accumulate if we execute it thousands of times.

We can save the expression in a field and then call the ExecuteOn method on this field. But this makes the code much less readable because the case definitions are in a different place from the actual execution point.

The Solution

There is a way to create 'static match expressions' - expressions which will be initialized only once.

The Match class contains the CreateStatic methods which allow the creation of static match expressions. Take a look at the modified example:

void DoStuff(int i) =>
    Match.CreateStatic<int, string>(builder => builder
            .Case(...)
            .Case(...)
            .Case(...)
            .Case(...))
        .ExecuteOn(i);

It looks almost the same, except for one difference: the calls to Case methods are inside the lambda expression, called the build action, which is passed to the CreateStatic method. Now this match expression will be initialized only once, and its initialization code is in the same place as its execution point.

The parameter of the build action has the type MatchBuilder<TInput, TOutput or MatchBuilder<TInput> depending on which type of match expressions you are building. This type has the same methods for adding cases as the Match classes and is mutable - the methods return the same builder instance.

MatchBuilder also has the Fallthrough method which specifies the default fall-through behavior. But this method specifies fall-through behavior only for cases that are defined after it. For example:

builder
    .Fallthrough(true)
    .Case(...) // this case has fall-through behavior if not specified otherwise
    .Case(...) // this case also has fall-through behavior if not specified otherwise
    .Fallthrough(false)
    .Case(...) // this case doesn't have fall-through behavior if not specified otherwise
    .Case(...) // this case also doesn't have fall-through behavior if not specified otherwise

Every case can configure its fall-through behavior individually as well.

Caching Match Expressions

The build action will be called only once, and its result will be cached. The cache is a static hash-table. The caching process is not thread-safe.

The key of the cache is the place where the CreateStatic method is called. Apart from the build action, this method also accepts two caller info arguments: the path to the source file and the line in the source file. Users don't need to pass these arguments to the method as they have the CallerFilePath and CallerLineNumber attributes.

The Match class also contains the ClearCache methods which clear a global cache. Match expressions have a cache per type (so Match<int, int> uses a different cache than Match<int, string>) so ClearCache only clears one cache. Clearing the cache will force all static match expressions of that type to be reinitialized. This process is not thread-safe as well.

Next article: Asynchronous pattern matching

  • Improve this Doc
In This Article
Back to top
Copyright (c) 2018-2021 Anatoliy Pylypchuk
Generated by DocFx. Icons made by Roundicons from www.flaticon.com is licensed by CC 3.0 BY