Click here to Skip to main content
13,792,910 members
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

28.2K views
240 downloads
67 bookmarked
Posted 3 May 2018
Licenced CPOL

Stop Writing Switch and If-Else Statements!

, 4 May 2018
Rate this:
Please Sign up or sign in to vote.
Fun with Tuples and Extension Methods to Implement a Match Function

Introduction

While this article is somewhat tongue-in-cheek, it's also an interesting exploration into what you can do with tuples, extension methods, and variable parameters. I thought I'd see just how far I could take this, and this article is the result.

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 Variations

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)
  {
    matches.Add((qualifier, 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 variations. 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 no 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)
  {
    if (await Task.Run(() => match.qualifier(val)))
    {
      await Task.Run(() => match.action(val));
      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")))
);

Console.WriteLine("Ready!");
Console.ReadLine();

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
Ready!
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Marc Clifton
United States United States
Marc is the creator of two open source projects, MyXaml, a declarative (XML) instantiation engine and the Advanced Unit Testing framework, and Interacx, a commercial n-tier RAD application suite.  Visit his website, www.marcclifton.com, where you will find many of his articles and his blog.

Marc lives in Philmont, NY.

You may also be interested in...

Comments and Discussions

 
QuestionIt's Great But... Pin
Hyland Computer Systems25-Jun-18 9:30
memberHyland Computer Systems25-Jun-18 9:30 
QuestionMy vote of 5* Pin
Bojan Sala25-Jun-18 3:32
professionalBojan Sala25-Jun-18 3:32 
GeneralMy vote of 4 Pin
tbayart25-Jun-18 2:50
membertbayart25-Jun-18 2:50 
Praisevery nice Pin
BillW3318-Jun-18 6:17
professionalBillW3318-Jun-18 6:17 
QuestionAs always excellent Pin
Mike Hankey8-Jun-18 5:01
professionalMike Hankey8-Jun-18 5:01 
SuggestionWhy not a dictionary? It already matches. Pin
rhyous11-May-18 6:08
memberrhyous11-May-18 6:08 
GeneralRe: Why not a dictionary? It already matches. Pin
Marc Clifton14-May-18 11:26
protectorMarc Clifton14-May-18 11:26 
GeneralRe: Why not a dictionary? It already matches. Pin
rhyous21-May-18 7:17
memberrhyous21-May-18 7:17 
QuestionElse or Default statement? Pin
dbrenth9-May-18 4:20
memberdbrenth9-May-18 4:20 
AnswerRe: Else or Default statement? Pin
Marc Clifton9-May-18 8:15
protectorMarc Clifton9-May-18 8:15 
Questionwhat is the conclusion? Pin
Debashis 104336567-May-18 1:16
memberDebashis 104336567-May-18 1:16 
AnswerRe: what is the conclusion? Pin
wkempf7-May-18 2:04
memberwkempf7-May-18 2:04 
AnswerRe: what is the conclusion? Pin
Marc Clifton7-May-18 3:02
protectorMarc Clifton7-May-18 3:02 
QuestionMy vote of #5, and one comment/idea Pin
BillWoodruff7-May-18 1:14
mentorBillWoodruff7-May-18 1:14 
GeneralMy vote of 5 Pin
Bryian Tan4-May-18 11:13
mvpBryian Tan4-May-18 11:13 
GeneralRe: My vote of 5 Pin
Marc Clifton5-May-18 11:05
protectorMarc Clifton5-May-18 11:05 
PraiseRule engine Pin
RickZeeland4-May-18 1:20
mvpRickZeeland4-May-18 1:20 
GeneralRe: Rule engine Pin
Marc Clifton4-May-18 3:20
protectorMarc Clifton4-May-18 3:20 
QuestionNice one Pin
Sacha Barber3-May-18 23:29
mvpSacha Barber3-May-18 23:29 
SuggestionPerformance Pin
Ciprian Beldi3-May-18 21:15
memberCiprian Beldi3-May-18 21:15 
GeneralRe: Performance Pin
wkempf4-May-18 2:51
memberwkempf4-May-18 2:51 
GeneralRe: Performance Pin
Marc Clifton4-May-18 4:44
protectorMarc Clifton4-May-18 4:44 
GeneralRe: Performance Pin
wkempf4-May-18 5:39
memberwkempf4-May-18 5:39 
GeneralRe: Performance Pin
Marc Clifton4-May-18 4:45
protectorMarc Clifton4-May-18 4:45 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web05 | 2.8.181207.3 | Last Updated 4 May 2018
Article Copyright 2018 by Marc Clifton
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid