📜 ⬆️ ⬇️

Another Pattern Matching in C # - now with context building

A month and a half ago, I published an article dedicated to the implementation of a sample reference in C #. In the commentary to the article, gBear rightly noted the lack of context in the cases. In the first version of the matcher, I deliberately ignored this mechanism, since I considered the syntactic possibilities of expressions in C # insufficient for its implementation. However, some time later, I realized that the desired effect can be achieved by building an Expression manually. Under the cut - the implementation of high-grade pattern matching.

Initially, when implementing pattern matching, I wanted to make the case-expression syntax look like the following:
s => string.IsNullOrEmpty(s) => 0 

Unfortunately, in C # this is syntactically incorrect: in fact
 s => t => s * t 

It is a function in curried form. The second idea for case expressions was to use a ternary operator like the following:
 s => t ? a : b 

Which again is impossible due to the lack of the Unit type in C # (for use in the else branch). There was an idea for the type for the expression b to make Expression <..> and pass the next case there, but this is prevented by the requirement of type identity for the expressions a and b.

At some point I got used to the idea that I would not succeed in realizing contextual connectedness and used the match in the form in which it exists.
Once in the process of debugging code like the following
 ... {s => s is string, s => ((string)s).Length} ... 

I thought that instead of checking the type is, we could do the as conversion and check the result of this conversion. Then it dawned on me - after all, this is essentially the construction of the context! Without delay, I took up the implementation.

In the second version, it was decided to completely abandon the implementation of the Matcher by searching the lambda functions and use only expression trees (as in ExprMatcher). The Add method had to be typed:
')
 public void Add<TCtx>(Expression<Func<TIn, TCtx>> binder, Expression<Func<TCtx, TOut>> processor) { var bindResult = Expression.Variable(typeof (TCtx), "binded"); var caseExpr = Expression.Block( new []{bindResult}, Expression.Assign(bindResult, Expression.Invoke(binder, Parameter)), Expression.IfThen( Expression.NotEqual(Expression.Convert(bindResult, typeof(object)), Expression.Constant(null)), Expression.Return(RetPoint, Expression.Invoke(processor, bindResult)) )); _caseExpressionsList.Add(caseExpr); } 

The TCtx type is the “context type” for a case. If the first expression returns a non-null instance of TCtx, the second expression is executed, and the argument for it is the result of the match.
It was decided to leave the previous syntax with predicates, since it is sometimes more convenient:
 public void Add(Expression<Predicate<T>> condition, Expression<Action<T>> processor) { var caseExpr = Expression.Block( new Expression[]{ Expression.IfThen( Expression.Invoke(condition, Parameter), Expression.Return(RetPoint, Expression.Invoke(processor, Parameter)) )}); _caseExpressionsList.Add(caseExpr); } 

Since case expressions are now built directly when added, the assembly code for the full matcher expression is much simpler:
 private Func<TIn, TOut> CompileMatcher() { var finalExpressions = new Expression[] { Expression.Throw(Expression.Constant(new MatchException("Provided value was not matched with any case"))), Expression.Label(RetPoint, Expression.Default(typeof(TOut))) }; var matcherExpression = Expression.Block(_caseExpressionsList.Concat(finalExpressions)); return Expression.Lambda<Func<TIn, TOut>>(matcherExpression, Parameter).Compile(); } 


I will give a small example of a function that returns the value of the argument, if its type is string or StringBuilder and the string “Unknown object” - otherwise:
 var match = new Matcher<object, string> { {s => s as string, s => s}, {sb => sb as StringBuilder, sb => sb.ToString()}, {o => true, (bool _) => "Unknown object"} }.ToFunc(); 


Future plans include adding a set of ready-made discriminators for common cases to the package.
On this, perhaps, everything. Just in case, I will provide links to the project and the matcher nuget-package:
Project on bitbucket
Nuget package

Like last time, I will be glad to comments / suggestions in the comments.
Thanks for attention!

Source: https://habr.com/ru/post/227935/


All Articles