Pattern matching in C# – part 1

In my previous post, I tried to explain what pattern matching is, and in particular the F# match expression, that I’m trying to mimic in C#. For my first attempt, I’ll try to analyse the matching cases that can apply on a single value. Let’s start right now with an F# sample[1]:

let test str =
    match str with
    | "TEST" -> Math.PI
    | "TEST_1" -> Math.E
    | null -> 0.0
    | s when s.StartsWith("TEST__") -> Math.E * 2.0
    | s -> float s.Length

If we analyse these patterns, we see 3 different cases:

  • a constant pattern (including the null value),
  • a conditional pattern (the lambda one)
  • a default pattern (the else case).

So in C#, what I’ve managed to build is the following:

var stringPattern = PatternMatcher
.CreateSwitch<string, double>()
.Case("TEST"                        , s => Math.PI)
.Case("TEST_1"                      , s => Math.E)
.Case((string)null                  , s => 0.0)
.Case(s => s.StartsWith("TEST__")   , s => Math.E * 2)
.CaseElse(                            s => s.Length);

What is difficult here is to write something that is valid C#, because:

  1. I can’t change the compiler,
  2. To keep things simple, I don’t want to use Roslyn in the first attempt (although that might come later).

Recording expressions for individual tests

This solution is mainly based upon a generic class called PatternMatcher<TSource, TResult>, which represents the match expression being built. Each Case overload builds an instance of a MatchCase class. As with many “fluent” syntax nowadays, in order to chain the calls in a single statement, each Case overload returns a new instance of the PatternMatcher, which keeps track of all the cases registered so far.

The MatchCase’s sole purpose is to encapsulate two expressions:

  • a test expression that will be used to determine if a particular source value matches the case: this will be an expression of Func<TSource, bool>,
  • a selector expression that will be used to produce the result of the match expression when the input value matches this particular case: this will be an expression of Func<TSource, TResult>.

Getting an Expression of a given Func type is nothing more that changing the type of a variable. The compiler will change its work accordingly, and produce an Expression from a given lambda-expression, instead of a typed delegate.

The most general case is the conditional pattern. Let’s take a look at the following extension method:

public static PatternMatcher<TSource, TResult>
    Case<TSource, TResult>(
        this PatternMatcher<TSource, TResult> pattern,
        Expression<Func<TSource, bool>> testExpression,
        Expression<Func<TSource, TResult>> selector)

You can see that the testExpression and selector parameters are already the types that my MatchCase class needs. From this values, I just have to instantiate the class and store it for later use.

The two other  cases (constant and default) will also provide directly the selector expression, only the test expression will have to be provided:

  • as an expression that tests equality with a given constant, for the constant pattern,
  • as an expression that is always true, for the default pattern.

These cases are just special cases of the previous one, called with an appropriate lambda, which will be converted to the test expression.

Building an expression tree

Of course, what we want to do is more that just record the expressions ! The next step is to combine them in a global match expression.

The kind of code I would like to produce, for the previous sample, would be the following:

Func<string, double> generatedFunc =
(string s) =>
        if (s == "TEST") return Math.PI;
        if (s == "TEST_1") return Math.E;
        if (s == null) return 0.0;
        if (s.StartsWith("TEST__")) return Math.E * 2;
        return s.Length;

Or even “better”, the same, but with conditional expressions instead of if and return statements:

Func<string, double> generatedFuncNoIf =
    (string s) =>
        (s == "TEST")
            ? Math.PI
            : (s == "TEST_1")
                ? Math.E
                : (s == null)
                    ? 0.0
                    : s.StartsWith("TEST__")
                        ? Math.E * 2
                        : s.Length;

An expression corresponding to the previous code could be generated, but I also want to handle an additional case: when there is no match. This means that my expression has to be able to return some result or… none. Some or none ? Isn’t there a type for that ? We’ll see how to use it next time.

[1] I know this sample is useless and theoretical, but isn’t this whole series ? If you really want to use match expressions, embrace F#, not C# !

This entry was posted in Functional Inspiration, Syntax Puzzles and tagged , , . Bookmark the permalink.

Comments are closed.