Skip to content
David Arno edited this page Jun 16, 2015 · 8 revisions

Pattern Matching

Succinc<T> pattern matching guide


Succinc<T>'s pattern matching features are heavily inspired by (ie, shamelessly stolen from) pattern matching in F#. As such, much of this guide simply re-uses F# examples from various pattern matching tutorials, attributing them as it goes.

One significant different between Succinc<T>'s pattern matching and F#'s is what happens when a case isn't covered. F# will give you a compiler error. Succinc<T> can't do this and so instead will throw a NoMatchException if no match is made when evaluated.

Succinc<T>'s built-in types (Union<T1, T2> ... Union<T1, T2, T3, T4>, Option<T> and ValueOrError all handle pattern matching in slightly different ways, and will be covered in other guides. This guide concentrates on pattern matching over other types.

Syntax

The generalised syntax for patterns can be expressed using BNF-like syntax. There are two types of match. Firstly, matching and returning a value:

result = {item}.Match().To<{result type}>()
              [.With({value})[.Or({value})]... .Do({item} => {result type expression} |
              [.Where({item} => {boolean expression}).Do({result type expression}]...
              [.Else({item} => {boolean expression})]
               .Result();

And the alternative is a match that invokes a void expression (ie, an Action<{item type}>):

{item}.Match()
     [.With({value})[.Or({value})]... .Do({void expression}) |
      .Where({item} => {void expression}).Do({void expression})]... 
     [.Else({item} => {void expression})]
      .Exec();

To explain the above syntax:

  • {} denotes a non-literal, eg {void expression} could be the empty expression, {}, or something like Console.WriteLine("hello").
  • Items in [] are optional.
  • | is or, ie [x|y] reads as "an optional x or y".
  • ... after [x] means 0 or more occurrences of x.

Basic Usage

The most basic form that can be used can be demonstrated with a boolean, as shown in the code examples below:

public static string YesOrNo(bool value)
{
    return value.Match().To<string>()
                .With(false).Do(x => "No")
                .With(true).Do(x => "Yes")
                .Result();
}

public static void PrintYesOrNo(bool value)
{
    value.Match()
         .With(false).Do(x => Console.WriteLine("No"))
         .With(true).Do(x => Console.WriteLine("Yes"))
         .Exec();
}

Note the slightly different syntax for the two methods. In the first case, we use .Match().To<string>() to specify we will return a string via a Func<int, string> function. If .To is missed off, then nothing is returned and instead an Action<int> function is executed instead.

In both cases, the pattern is to match specific values and to execute the matching lambda. Also in both cases, we have just stuck to using With, but we could simplify both by using Else:

public static string YesOrNoV2(bool value)
{
    return value.Match().To<string>()
                .With(false).Do(x => "No")
                .Else(x => "Yes")
                .Result();
}

public static void PrintYesOrNo(bool value)
{
    value.Match()
         .With(false).Do(x => Console.WriteLine("No"))
         .Else(x => Console.WriteLine("Yes"))
         .Exec();
}

Matching Multiple Values

For a boolean, there are only two values, so the pattern is simple. What about if we want to match many values for an int for example? We have two choices here: Or and Where

Firstly, using Or we could write a function to check for example, a single digit odd number like this:

public static bool IsSingleDigitAndOdd(int value)
{
    return value.Match().To<string>()
                .With(1).Or(3).Or(5).Or(7).Or(9).Do(x => true)
                .Else(x => false)
                .Result();
}

If we simply wanted to check for a single-digit positive number though, we can use Where:

public static bool IsSingleDigitAndOdd(int value)
{
    return value.Match().To<string>()
                .Where(x => x > 0 && x < 10).Do(x => true)
                .Else(x => false)
                .Result();
}

Match Order

So far, we've only looked at examples with two match patterns. In many cases though, more may be required and the match patterns can overlap. The following function highlights this:

public static string OddOrPositive(int value)
{
    return value.Match().To<string>()
                .Where(x => x % 2 == 1).Do(i => string.Format("{0} is odd", i))
                .Where(x => x > 0).Do(i => string.Format("{0} is positive", i))
               .Else(i => string.Format("{0} is neither odd, nor positive"))
               .Result();
}

Clearly in this situation, all positive odd integers will match both Where clauses. The matching mechanism though will try each match in the order specified and stop on the first match. So OddOrPositive(1) will return 1 is odd, rather than 1 is positive.

Matching enums

Enums can be matched just like any other value type, for example:

public static void PrintColorName(Color color)
{
    color.Match()
         .With(Color.Red).Do(x => Console.WriteLine("Red"))
         .With(Color.Green).Do(x => Console.WriteLine("Green"))
         .With(Color.Blue).Do(x => Console.WriteLine("Blue"))
         .Exec();
}

Matching Reference types

Succinc<T> doesn't do anything clever with its comparisons, it just uses the default equality function for a type. So if a type compares by reference, the pattern will match on reference. So, for the following code:

class Point
{
    int X { get; set; }
    int Y { get; set; }
}

class Example
{
    private p1 = new Point { X = 1, Y = 2 };
    private p2 = new Point { X = 1, Y = 2 };

    public bool P1P2AreEqual()
    {
        return p1.Match().To<bool>()
                 .With(p2).Do(x => true)
                 .Else(x => false)
                 .Result();
    }
}

Calling P1P2AreEqual() will return false.

Real World Example

Clearly the reference type example above is hiighly contrived, as P1P2AreEqual() is just a long-winded way of doing p1 == p2. But pattern-matching has real uses too, as the following example shows. This is a translation of an F# example:

public static int Fib(int n)
{
    return n.Match().To<int>()
            .With(0).Or(1).Do(x => x)
            .Else(x => Fib(x - 1) + Fib(x - 2))
            .Result();
}

This is a simple, concise method that uses pattern matching and recursion to "solve" the fibonacci sequence.