Hello again, you potential RSS subscriber (probably still reading this in Google Reader, trying to determine which reader you’ll choose next) ! Last time I left you with a working pattern-matching mechanism in C#, but very limited… as it could only match a single value at a time.
What makes the pattern-matching mechanism so powerful in functional languages is its ability to match many sorts of data structures, amongst which are tuples.
Defining a match expression on a tuple or 2 values in F# can be done this way :
let result = match input with | 1, 1 -> 1 | 2, _ -> 2 | _, 3 -> 3 | a, b when a > 4 -> 4 | _ -> 5
This sample shows some interesting cases. A tuple of 2 values can be matched with several expressions, each value of the tuple being compared with individual patterns.
In part 1 of this series, we stated these individual patterns:
- a constant pattern,
- a conditional pattern,
- a default pattern, known as the wildcard pattern (the underscore in F#).
Now, we have to handle every possible combination of these patterns!
The following sample translated in C# in my experimental syntax is the following:
var tupleSample = PatternMatcher.CreateSwitch<int, int, int>() .Case(1 , 1, (a, b) => 1) .Case(2 , _, (a, b) => 2) .Case(_ , 3, (a, b) => 3) .Case(a => a > 4, _, (a, b) => 4) .Case(_ , _, (a, b) => 5);
Welcome to the parenthesis hell. Soon it will like like Lisp if we go on… But how do we enable that syntax ?
In order to handle gracefully the wildcard (“_”) pattern, I have used a non-generic MatchCase class with a static field called Any:
public class MatchCase { public static readonly MatchCase Any; static MatchCase() { Any = new MatchCase(); } private MatchCase() { } }
This trick allows me to define methods with a MatchCase-typed parameter to handle the wildcard pattern case. Moreover, if I add the following line before my patterns definitions :
var _ = MatchCase.Any;
I’m then able to use a simple “_” to represent the “any” case.
The method used previously to build the pattern matching Func and its invocation in the Eval method doesn’t change: only the “recording” process is impacted. The pain point here is that we need to define each necessary overload of the Case extension method, in order to handle all the patterns. These extensions methods are all based on the following model:
public static PatternMatcher<Tuple<T1, T2>, TResult> Case<T1, T2, TResult>( this PatternMatcher<Tuple<T1, T2>, TResult> pattern, {arg1}, {arg2}, Expression<Func<T1, T2, TResult>> selector)
where the {arg1} and {arg2} placeholders are replaced with :
- T1 testValue1, T2 testValue2
- T1 testValue1, Expression<Func<T2, bool>> testExpression2
- T1 testValue1, MatchCase any2
- Expression<Func<T1, bool>> testExpression1, T2 testValue2
- Expression<Func<T1, bool>> testExpression1, Expression<Func<T2, bool>> testExpression2
- Expression<Func<T1, bool>> testExpression1, MatchCase any2
- MatchCase any1, T2 testValue2
- MatchCase any1, Expression<Func<T2, bool>> testExpression2
- MatchCase any1, MatchCase any2
The implementation of each method follows the same pattern as in the previous post, and builds an expression that determines whether a given tuple matches the pattern, returning, and evaluates the selector expression when there is a match.
But if we want to handle tuples with more values… do we have to write all the overloads ?!