Skip to content

Pattern matching

Leonid Gordo edited this page Aug 18, 2016 · 15 revisions

The one of the fundamental thing in functional way programming is the pattern matching. Unfortunately, the C# does not provide a native support of this concept. To fill this gap the Functional.Fluent library offers the implementation of this feature including type pattern matching and list pattern matching.

Value pattern matching

The most obvious way to imitate the matching is to use ApplyIf bunch of methods. Look at the example below:

var s = "fox";
var r = s.ToM()
   .ApplyIf(x => x == "bird", x => x + " can fly")
   .ApplyIf(x => x == "fox", x => x + " can run");

This is the very first approach that works, but the library gives you the dedicated type for matching: Matcher. The use is very straightforward:

var m = new Matcher<int, string>
{
    {x => x % 2 == 0, x => x.ToString() + " is even"},
    {x => true, x => x.ToString() + " is odd"}  
};
var r = m.Match(4);

If you want to match just against the exact values then review a simplified form:

var m = new Matcher<int, string>
{
    {5, x => x.ToString() + " is five"},
    {4, x => x.ToString() + " is four"},
    {3, x => x.ToString() + " is three"},
    {x => true, x => x.ToString() + " is something other"}  
};
var r = m.Match(4);

The case can accept not only the single value but the collection of values:

var m = new Matcher<int, string>
{
    {new[] {2, 4, 6}, x => x.ToString() + " is two or four or six"},
    {new[] {3, 5, 7}, x => x.ToString() + " is three or five or seven"},
    {x => true, x => x.ToString() + " is something other"}  
};
var r = m.Match(i);

Fluent syntax

The library follow the concept of fluent code style, so it tries to provide the ability to write code in fluent way for every bit of it. The same is true for pattern matching feature.

result = "fox".ToM().Match()
            .With(x => x == "bird", x => x + " can fly")
            .With(x => x == "fox", x => x + " can run")
            .Else(x => x + " can do something else")
            .Do();

The same works for exact values:

result = "fox".ToM().Match()
            .With("bird", x => x + " can fly")
            .With("fox", x => x + " can run")
            .Else(x => x + " can do something else")
            .Do();

The matcher can be converted to Func<> to be called later on.

 var m = new Matcher<string, string>
 {
     {x => x == "bird", x => x + " can fly"},
     {x => x == "fox", x => x + " can run"},
     {x => true, x => x + " can do something else"},
  }.ToFunc();

 ...

 m("bird");   // here is matching actually runs

The hello world for pattern matching is the factorial calculation. Let's review this example. We define factorial function:

int factorial(int n) => n.ToM().Match()
        .With(0, 1, _ => 1)
        .Else(v => v* factorial(v - 1))
        .Do();

Now we can call this function:

result = factorial(5) // will return 120

Type pattern matching

To match the given type against the set of types use the TypeMatcher.

var m = new TypeMatcher<string>
{
    { Case.Is<string>(), s => s},
    { Case.Is<StringBuilder>(), s => s.ToString()}
};

string test1 = "a simple string";
m.Match(test1); // returns "a simple string"

Or using fluent syntax:

result = ((object) "string").ToM().TypeMatch()
            .With(Case.Is<string>(), s => s)
            .With(Case.Is<StringBuilder>(), s => s.ToString())
            .Else(_ => "that's an object")
            .Do();

The type matching can be enriched with predicates. The predicate allows to add additional test for the case:

result = ((object)"the long string").ToM().TypeMatch()
            .With(Case.Is<string>(), s => s.Length > 10, s => s + "!")
            .With(Case.Is<string>(), s => s)
            .With(Case.Is<StringBuilder>(), s => s.ToString())
            .Else(_ => "that's an object")
            .Do();

List pattern matching

The library makes matching of the list elements as simple as matching of the scalar values.

var list = new [] { "good morning", "good afternoon", "good evening" };
var result = list.ToM().Match()
                 .With((h, t) => Tuple.Create(h.ToUpper(), t))
                 .Do(); 

// result is Tuple("GOOD MORNING", List<string> {"good afternoon", "good evening"})

To check if the list is empty you can use Empty case:

var list = new List<string>();
var result = list.ToM().Match()
            .With((h, t) => h.ToUpper())
            .Empty(() => "List is empty")
            .Do();

Let's review the example of the function that sums the even number in the list:

int sum_even(IEnumerable<int> value) => value.ToM().Match()
       .With(v => v % 2 == 0, (head, tail) => head + sum_even(tail))
       .With((_, tail) => sum_even(tail))
       .Empty(() => 0)
       .Do();
Clone this wiki locally