14,635,966 members
Articles » General Programming » Algorithms & Recipes » Algorithms » Revisions
Article
Posted 3 May 2018

45.1K views
72 bookmarked

# Stop Writing Switch and If-Else Statements!

Rate this:
4 May 2018CPOL
Fun with Tuples and Extension Methods to Implement a Match Function
This is a new version of the currently published article.

## Introduction

This code requires C# 7.1 at a minimum!

F# has a nifty match expression, that looks like this:

```match [something] with
| pattern1 -> expression1
| pattern2 -> expression2
| pattern3 -> expression3```

Could we do something similar to this in C#?

Certainly!  In fact, the `improved` switch keyword can evaluate expressions and perform pattern matching.  For example:

• expression evaluation: `case var testAge when ((testAge >= 90) & (testAge <= `99)):
• pattern matching: `case Planet p:`

OK, that's cool, but let's explore something a wee bit more like the F# syntax.

## Four Variation

I'll walk you through the four variations that I implemented, starting with the first proof of concept and ending with the fourth "ah, this is the solution."  Then we'll look at some other cool things you can do with this.

### Pass 1 -- Adding Pattern and Expressions to a Match Collection

I wanted to start with something basic, so I implemented a `Rule` class:

```public class Rule<T>
{
protected List<(Func<T, bool> qualifier, Action<T> action)> matches = new List<(Func<T, bool> qualifier, Action<T> action)>();

public Rule<T> MatchOn(Func<T, bool> qualifier, Action<T> action)
{

return this;
}

public Rule<T> Match(T val)
{
foreach (var match in matches)
{
if (match.qualifier(val))
{
match.action(val);
break;
}
}

return this;
}
}```

And it's used like this:

```var rule = new Rule<int>()
.MatchOn(n => n == 0, _ => Console.WriteLine("Zero"))
.MatchOn(n => n == 1, _ => Console.WriteLine("One"))
.MatchOn(n => n == 2, _ => Console.WriteLine("Two"));

rule.Match(0);```

The output is of course "Zero".  This really isn't what I want because we're creating a collection every time we run the rule (unless we save the rules somewhere, yuck) and it's verbose.  But it's a proof of concept of the overall `pattern -> expression` syntax that we see in F#.

### Pass 2 - Using an Extension Method

My second attempt gets rid of the Rule class but has a major flaw -- every expression is evaluated even if a match is found.  This is a rather brain dead solution, but here it is:

```public static class ExtensionMethods
{
public static (bool, T) Match<T>(this T item, Func<T, bool> qualifier, Action<T> action)
{
return (false, item).Match(qualifier, action);
}

public static (bool, T) Match<T>(this (bool hasMatch, T item) src, Func<T, bool> qualifier, Action<T> action)
{
bool hasMatch = src.hasMatch;

if (!hasMatch)
{
hasMatch = qualifier(src.item);

if (hasMatch)
{
action(src.item);
}
}

return (hasMatch, src.item);
}
}```

Note how each expression returns a tuple indicating whether a match is found, and the source item.  Using tuples is the one consistent thing in each of these variation.  A couple usage examples:

```(false, 1)
.Match(n => n == 0, _ => Console.WriteLine("Zero"))
.Match(n => n == 1, _ => Console.WriteLine("One"))
.Match(n => n == 2, _ => Console.WriteLine("Two"));

2
.Match(n => n == 0, _ => Console.WriteLine("Zero"))
.Match(n => n == 1, _ => Console.WriteLine("One"))
.Match(n => n == 2, _ => Console.WriteLine("Two"));```

The one advantage that this approach has is that I'm not longer creating a collection of `pattern -> expression` cases -- these are evaluated "live."

### Pass 3 - Static Method with Variable Parameters

So then I went back to the simpler case of having a class with a static method, which is basically like an extension method, it's just that this form is sometimes easier on the brain.  It looks like this:

```public static class Matcher<T>
{
public static void Match(T val, params (Func<T, bool> qualifier, Action<T> action)[] matches)
{
foreach (var match in matches)
{
if (match.qualifier(val))
{
match.action(val);
break;
}
}
}
}  ```

and is used like this:

```Matcher<int>.Match(3,
(n => n == 0, _ => Console.WriteLine("Zero")),
(n => n == 1, _ => Console.WriteLine("One")),
(n => n == 2, _ => Console.WriteLine("Two")),
(n => n == 3, _ => Console.WriteLine("Three")));```

OK, now we're getting somewhere.  We've eliminated the Match method name for each `pattern -> expression`, but we still have the class name, the static method name, and even worse, we have to specify the generic parameter type.  So, one step forward and two steps backward.

### Pass 4 - Extension Methods to the Rescue Again

In this version, I get rid of the static class method by using an extension method, but in this case the extension method has a variable number of parameters:

```public static void Match<T>(this T val, params (Func<T, bool> qualifier, Action<T> action)[] matches)
{
foreach (var match in matches)
{
if (match.qualifier(val))
{
match.action(val);
break;
}
}
}```

And the usage example:

```2.Match(
(n => n == 0, _ => Console.WriteLine("Zero")),
(n => n == 1, _ => Console.WriteLine("One")),
(n => n == 2, _ => Console.WriteLine("Two")),
(n => n == 3, _ => Console.WriteLine("Three")),
(n => n == 4, _ => Console.WriteLine("Four")));```

Snazzy!  That is the closest to the F# syntax.

## What Else Can we Do?

### MatchAll

Here's something you can't do with an F# `match` or C# `switch` statement: MatchAll!

```public static void MatchAll<T>(this T val, params (Func<T, bool> qualifier, Action<T> action)[] matches)
{
foreach (var match in matches)
{
if (match.qualifier(val))
{
match.action(val);
}
}
}```

All we've done here is eliminate the break statement.  Usage example:

```10.ForEach(n => n.MatchAll(
(v => v % 2 == 0, v => Console.WriteLine(\$"{v} is even.")),
(v => v % 2 == 1, v => Console.WriteLine(\$"{v} is odd.")),
(_ => true,       v => Console.WriteLine(\$"{v} * 10 = {v * 10}."))
));```

Screams will erupt regarding the `10.ForEach`, I'm sure.  Also, if you're not familiar with the "\$" notation (I rarely use it myself), look up string interpolation.  Anyways, here's the output:

```0 is even.
0 * 10 = 0.
1 is odd.
1 * 10 = 10.
2 is even.
2 * 10 = 20.
3 is odd.
3 * 10 = 30.
4 is even.
4 * 10 = 40.
5 is odd.
5 * 10 = 50.
6 is even.
6 * 10 = 60.
7 is odd.
7 * 10 = 70.
8 is even.
8 * 10 = 80.
9 is odd.
9 * 10 = 90.```

I personally find that pretty snazzy.

### MatchAsync

This might be useful for a pattern or expression that takes a while to execute, perhaps some sort of an I/O operation or DB query:

```public async static void MatchAsync<T>(this T val, params (Func<T, bool> qualifier, Action<T> action)[] matches)
{
foreach (var match in matches)
{
{
break;
}
}
}```

And a simplistic (and weird) example:

```Console.WriteLine(DateTime.Now.ToString("hh:MM:ss"));

1000.MatchAsync(
(v => { Thread.Sleep(v); return true; }, _ => Console.WriteLine(DateTime.Now.ToString("hh:MM:ss")))
);

The output is what you'd expect -- the time is displayed, the match falls through as the task is run, then the continuation of the `async` operation is performed:

```08:05:09
08:05:10```

Now that is quite interesting!

### MatchReturn

As wkempf commented in the message section: "In F# match is an expression (it returns a value) and not a statement" we can implement a match extension method that returns a value:

```public static U MatchReturn<T, U>(this T val, params (Func<T, bool> qualifier, Func<T, U> func)[] matches)
{
U ret = default(U);

foreach (var match in matches)
{
if (match.qualifier(val))
{
ret = match.func(val);
break;
}
}

return ret;
}```

Usage when all the return types are the same:

```string ret = 2.MatchReturn(
(n => n == 0, _ => "Zero"),
(n => n == 1, _ => "One"),
(n => n == 2, _ => "Two"),
(n => n == 3, _ => "Three"),
(n => n == 4, _ => "Four"));

Console.WriteLine(ret);```

Usage when the expression returns different types:

```5.ForEach(q =>
{
dynamic retd = q.MatchReturn<int, dynamic>(
(n => n == 0, _ => "Zero"),
(n => n == 1, n => n),
(n => n == 2, n => new BigInteger(n)),
(n => n == 3, n => new Point(n, n)),
(n => n == 4, n => new Size(n, n)));
Console.WriteLine(retd.ToString());
});```

Output:

```Zero
1
2
{X=3,Y=3}
{Width=4, Height=4}```

Because the return type cannot be inferred, note that we have to explicitly provide the generic parameters specifying the input type and the `dynamic` return type.

## Performance

Regarding the question of performance, a simplistic performance tester:

```DateTime start = DateTime.Now;
1000000.ForEach(q => (q % 5).MatchReturn(
(n => n == 0, _ => "Zero"),
(n => n == 1, _ => "One"),
(n => n == 2, _ => "Two"),
(n => n == 3, _ => "Three"),
(n => n == 4, _ => "Four")));
DateTime stop = DateTime.Now;
Console.WriteLine("MatchReturn for 1,000,000 runs took " + (stop - start).TotalMilliseconds + "ms");

string sret;
start = DateTime.Now;
1000000.ForEach(q =>
{
switch (q % 5)
{
case 0:
sret = "Zero";
break;
case 1:
sret = "One";
break;
case 2:
sret = "Two";
break;
case 3:
sret = "Three";
break;
case 4:
sret = "Four";
break;
}
});
stop = DateTime.Now;
Console.WriteLine("Switch for 1,000,000 runs took " + (stop - start).TotalMilliseconds + "ms");```

Shows how poor performing (by a factor of 22) this implementation is compared to a simple numeric switch:

```MatchReturn for 1,000,000 runs took 224ms
Switch for 1,000,000 runs took 7.9983ms```

## Conclusion

Personally, I find that what started off as a silly idea became more and more interesting, particularly with the `MatchAll` and `MatchAsync` implementation, which I've found to already be useful in an application I'm developing.  But yes, I do walk on the wild side.

## Share

 Architect Interacx United States
Blog: https://marcclifton.wordpress.com/
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

 First PrevNext
 Great article, I think Microsoft read this too Sander Rossel18-Jul-20 22:53 Sander Rossel 18-Jul-20 22:53
 It's Great But... Hyland Computer Systems25-Jun-18 8:30 Hyland Computer Systems 25-Jun-18 8:30
 Re: It's Great But... Marc Clifton25-Jan-19 7:49 Marc Clifton 25-Jan-19 7:49
 My vote of 5* Bojan Sala25-Jun-18 2:32 Bojan Sala 25-Jun-18 2:32
 My vote of 4 tbayart25-Jun-18 1:50 tbayart 25-Jun-18 1:50
 very nice BillW3318-Jun-18 5:17 BillW33 18-Jun-18 5:17
 As always excellent Mike Hankey8-Jun-18 4:01 Mike Hankey 8-Jun-18 4:01
 Why not a dictionary? It already matches. rhyous11-May-18 5:08 rhyous 11-May-18 5:08
 Re: Why not a dictionary? It already matches. Marc Clifton14-May-18 10:26 Marc Clifton 14-May-18 10:26
 Re: Why not a dictionary? It already matches. rhyous21-May-18 6:17 rhyous 21-May-18 6:17
 Else or Default statement? dbrenth9-May-18 3:20 dbrenth 9-May-18 3:20
 Re: Else or Default statement? Marc Clifton9-May-18 7:15 Marc Clifton 9-May-18 7:15
 what is the conclusion? Debashis 104336567-May-18 0:16 Debashis 10433656 7-May-18 0:16
 Re: what is the conclusion? wkempf7-May-18 1:04 wkempf 7-May-18 1:04
 Re: what is the conclusion? Marc Clifton7-May-18 2:02 Marc Clifton 7-May-18 2:02
 My vote of #5, and one comment/idea BillWoodruff7-May-18 0:14 BillWoodruff 7-May-18 0:14
 My vote of 5 Bryian Tan4-May-18 10:13 Bryian Tan 4-May-18 10:13
 Re: My vote of 5 Marc Clifton5-May-18 10:05 Marc Clifton 5-May-18 10:05
 Rule engine RickZeeland4-May-18 0:20 RickZeeland 4-May-18 0:20
 Re: Rule engine Marc Clifton4-May-18 2:20 Marc Clifton 4-May-18 2:20
 Nice one Sacha Barber3-May-18 22:29 Sacha Barber 3-May-18 22:29
 Performance Ciprian Beldi3-May-18 20:15 Ciprian Beldi 3-May-18 20:15
 Re: Performance wkempf4-May-18 1:51 wkempf 4-May-18 1:51
 Re: Performance Marc Clifton4-May-18 3:44 Marc Clifton 4-May-18 3:44
 Re: Performance wkempf4-May-18 4:39 wkempf 4-May-18 4:39
 Last Visit: 21-Sep-20 2:17     Last Update: 21-Sep-20 2:17 Refresh 12 Next ᐅ