DISCLAIMER: this post has been written months ago and never posted. By that time I was still trying to bring F# goodness to C#. This might be easier today with Roslyn, but that is not the path I’ve taken. Since the last post of this series, I’ve been doing more and more F#, even during my day job. I don’t think I’ll ever post this kind of ‘’How to do F# in C#” posts. What you can expect from this blog is now mostly functional programming in F#…
Today’s post will be a short one, as an answer to question from several months ago:
But if we want to handle tuples with more values… do we have to write all the overloads ?!
The answer is yes. With the current implementation, there has to be a method overload for every combination of patterns that you want to be able to match…
But wait a minute… For a given tuple of n values, there are 3n combinations… That makes a lot of methods to write ?!
You don’t have to actually write all these overloads yourself! T4 templates are there exactly for that purpose. If we want to generate all the extensions methods for the tuples of 2 elements up to 4 elements, for instance, we just have to build a .tt file, and define the 3 individual patterns as a string array :
string[] argumentsPatterns = new[] { "T{0} testValue{0}", "Expression<Func<T{0}, bool>> testExpression{0}", "MatchCase any{0}" };
Then and include a first loop :
for(int cardinality = 2; cardinality <= 3; cardinality++) { [...]
As we are going to write the type arguments quite a few times, we store them in a string, as well as the type of the selector argument:
string typeArguments = string.Join( ", ", Enumerable.Range(1, cardinality) .Select(n => string.Concat("T", n.ToString()))); string selectorArgument = string.Concat( "Expression<Func<", typeArguments, ", TResult>> selector");
Inside the first loop we then loop over all the 3cardinality combinations :
for(int caseNumber = 0; caseNumber < System.Math.Pow(3, cardinality); caseNumber++) { [...]
And we decompose the caseNumber into an array of int whose values can either be 0, 1 or 2, corresponding to the index of the pattern associated with each argument. For instance, with a cardinality of 4 and a caseNumber or 4, we would get an array containing [0, 0, 1, 1] :
int[] argumentsPatternsCases = new int[cardinality]; int argumentFlags = caseNumber; for(int argumentIndex = 0; argumentIndex < cardinality; argumentIndex++) { argumentsPatternsCases[cardinality - argumentIndex - 1] = argumentFlags % 3; argumentFlags = argumentFlags / 3; }
From all this, we can generate the signature of the method :
public static PatternMatcher<Tuple<<#= typeArguments #>>, TResult> Case<<#= typeArguments #>, TResult>( this PatternMatcher<Tuple<<#= typeArguments #>>, TResult> pattern, <# for(int argumentIndex = 0; argumentIndex < cardinality; argumentIndex++) { #> <#= string.Format(argumentsPatterns[argumentsPatternsCases[argumentIndex]], argumentIndex + 1) #>, <# } #> <#= selectorArgument #>)
To understand what happens for the body of the method, the best is to look at the generated code for a particular sample :
public static PatternMatcher<Tuple<T1, T2>, TResult> Case<T1, T2, TResult>( this PatternMatcher<Tuple<T1, T2>, TResult> pattern, Expression<Func<T1, bool>> testExpression1, T2 testValue2, Expression<Func<T1, T2, TResult>> selector) { ParameterExpression testParam = Expression.Parameter(typeof(Tuple<T1, T2>), "t"); MemberInfo[] members = GetTupleMembers<T1, T2>(); List<Expression> testExpressions = new List<Expression>(); testExpressions.Add( Expression.Invoke( testExpression1, Expression.MakeMemberAccess(testParam, members[0]))); testExpressions.Add( Expression.Equal( Expression.MakeMemberAccess(testParam, members[1]), Expression.Constant(testValue2))); Expression aggregateExpression = testExpressions.CombineAll(); var testExpression = Expression.Lambda<Func<Tuple<T1, T2>, bool>>( aggregateExpression, testParam); var selectorExpression = GetSelectorExpression<T1, T2, TResult>(selector, members); return new PatternMatcher<Tuple<T1, T2>, TResult>( pattern, new MatchCase<Tuple<T1, T2>, TResult>( testExpression, selectorExpression)); }
I’ve just extracted two methods here :
- an extension method CombineAll that combines test expressions into a single one by aggregating them using AndAlso expressions :
private static Expression CombineAll( this IEnumerable<Expression> expressions) { Expression aggregateExpression = expressions.Aggregate( (Expression)null, (a, e) => a == null ? e : Expression.AndAlso(a, e)); return aggregateExpression ?? Expression.Constant(true); }
- a GetSelectorExpression method, because the selector expression is generated exactly in the same way for each case number of a given cardinality.
The whole sample is available on my Github, including hand-crafted unit-tests for tuples of cardinality 2 and 3…
Dear Pierre,
Thanks for a very interesting series of articles on pattern matching. I would like to view the code but I could not find that on github (might have missed something obvious in that case sorry…)
Once again thanks for sharing your thoughts and work.
Best regards Anders
Sorry about that! I published this post and forgot to push the code on github! I’ll push it tonight and update my post to link to it. Thanks for your comment!