-
Notifications
You must be signed in to change notification settings - Fork 15
PatternMatchingTuples
This pattern matching guide is split into the following sections:
- Pattern matching on collections.
- Pattern matching on discriminated unions.
-
Pattern matching on
Either<TLeft, TRight>. - Pattern matching on
Option<T> - Pattern matching on
Success<T> - Pattern matching on tuples - Covered here.
- Pattern matching on
ValueOrErrorandValueOrError<TValue, TError> - Type-based pattern matching for all other types
- Value-based pattern matching on all other types
Pattern matching on tuples is achieved via a set of extension methods, Match<T1>() through Match<T1,T2,T3,T4>() that are associated with Tuple<T1> through Tuple<T1,T2,T3,T4>, and well as ValueTuple<T1> through ValueTuple<T1,T2,T3,T4>, respectively. Each then starts a fluent chain of functions that describe an action-based pattern matching expression. In other words, the method or lambda of type void A(Tuple<...> valueMatched) associated with the matching pattern is executed.
Obviously the reasons behind pattern matching is to supply a more functional style to the code. As such, it is common to want to return a value from the match. This is achieved by chaining the functions after Match().To<ResultType>(). The reason for using To<T>() rather than just providing a second Match variant, that returns T is due to the way C# handles generics and methods. The rule is simple: either all type parameters can be inferred and so none need be specified, or all type parameters must be specified. As such, the following style would have to be used when, in this case, matching on a tuple of int, string, DateTime and Color and returning a string:
var result = someTuple.Match<int, string, DateTime, Color, string>()...By using Match().To<ResultType>(), this becomes:
var result = someTuple.Match().To<string>()...It's a subtle change, but in my opinion it aids readability as the type parameter is focused solely on the return type, rather than restating the value/reference type to be matched.
The generalised syntax for patterns can be expressed using BNF-like syntax. As previously mentioned, there are two types of match. Firstly, matching and returning a value:
result = {tuple}.Match().To<{result type}>()
[WithExpression | WhereExpression ]...
[ElseExpression]
.Result();
WithExpression ==>
.With({v1},{v2}...)[.Or({v1},{v2}...)]... .Do({p1},{p2}... => {result type expression} |
.With({v1},{v2}...)[.Or({v1},{v2}...)]... .Do({result type expression})
WhereExpression ==>
.Where({v1},{v2}... => {boolean expression}).Do({p1},{p2}... => {result type expression} |
.Where({v1},{v2}... => {boolean expression}).Do({result type expression})
ElseExpression ==>
.Else({p1},{p2}... => {result type expression}) |
.Else({result type expression})
And the alternative is a match that invokes a void expression (ie, an Action<{item type}>):
{item}.Match()
[WithExpression | WhereExpression ]...
[ElseExpression]
.Exec();
WithExpression ==>
.With({v1},{v2}...)[.Or({v1},{v2}...)]... .Do({p1},{p2}... => {action}
WhereExpression ==>
.Where({v1},{v2}... => {boolean expression}).Do({p1},{p2}... => {action}
ElseExpression ==>
.Else({p1},{p2}... => {action}) |
.IgnoreElse()
To explain the above syntax:
-
{}denotes a non-literal, eg{void expression}could be the empty expression,{}, or something likeConsole.WriteLine("hello"). -
{v1},{v2}...Specifies the two - four parameters to match (according to the number of items in the tuple). Note that the wildcard__oranycan be used for any parameter to match any value for that parameter. - Items in
[]are optional. -
|isor, ie[x|y]reads as "an optional x or y". -
...after[x]means 0 or more occurrences of x. -
==>is a sub-rule, which defines the expression on the left of this symbol.
The most basic form that can be used can be demonstrated with two boolean's, as shown in the code examples below:
public static bool Xor((bool, bool) pair)
{
return pair.Match().To<bool>()
.With(false, false).Do(x, y => true)
.With(false, true).Do(x, y => false)
.With(true, false).Do(x, y => false)
.With(true, true).Do(x, y => true)
.Result();
}
public static void PrintXorResult(Tuple<bool, bool> pair)
{
value.Match()
.With(false, false).Do(x, y => Console.WriteLine("True"))
.With(false, true).Do(x, y => Console.WriteLine("False"))
.With(true, false).Do(x, y => Console.WriteLine("False"))
.With(true, true).Do(x, y => Console.WriteLine("True"))
.Exec();
}In the first case, we use .Match().To<bool>() to specify we will return a bool via a Func<bool, bool, bool> function. If .To is missed off, then nothing is returned and instead an Action<bool, bool> function is executed instead.
In both cases, the pattern is to match specific values and to execute the matching lambda. Also in both cases, we have just stuck to using With, but we could simplify both by using Else:
public static bool Xor((bool, bool) pair)
{
return pair.Match().To<bool>()
.With(false, false).Do(x, y => true)
.With(true, true).Do(x, y => true)
.Else(x, y => false)
.Result();
}
public static void PrintXorResult((bool, bool) pair)
{
value.Match()
.With(false, false).Do(x, y => Console.WriteLine("True"))
.With(true, true).Do(x, y => Console.WriteLine("True"))
.Else(x, y => Console.WriteLine("False"))
.Exec();
}One further change can be made to the functional example. We are supplying parameters, x and y, which aren't then used. In this case, we can dispense with the lambda and just specify the return value:
public static bool XorV2((bool, bool) pair)
{
return pair.Match().To<bool>()
.With(false, false).Do(true)
.With(true, true).Do(true)
.Else(false)
.Result();
}In the above examples, we have two patterns (false, false and true, true that both result in true. We can simplify this using Or:
public static bool XorV2((bool, bool) pair)
{
return pair.Match().To<bool>()
.With(false, false).Or(true, true).Do(true)
.Else(false)
.Result();
}With and Or work well for matching discrete values, but sometimes we want to match ranges of values. That's where Where comes in to play:
public static bool IsGray((int, int, int) rgb)
{
return rgb.Match().To<bool>()
.Where(r, b, g => r == b && r == g).Do(true)
.Else(false)
.Result();
}So far, we've only looked at examples with precise match patterns where there is no overlap of the patterns. In many cases though, the match patterns can overlap. The following function highlights this:
public static string BlackOrColors((int, int, int) rgb)
{
return value.Match().To<string>()
.Where(r, g, b => r == 0 && g == 0 && b == 0).Do("Black")
.Where(r, g, b => r == 0 && g == 0).Do("Blue")
.Where(r, g, b => r == 0 && b == 0).Do("Green")
.Where(r, g, b => g == 0 && b == 0).Do("Red")
.Else("Some other color")
.Result();
}Clearly in this situation, if r and g and b are all 0, all four Where clauses could be matched. The matching stops as soon as a pattern is matched though, so the result would be Black in a completely deterministic way.
Sometimes, we want to match tuples via a "for this value on Item1, and any value for Item2" pattern. For example, consider the following function:
public static bool IsFemale((Animal, Gender) details) =>
details.Match().To<bool>()
.Where(a, g => g == Gender.Female).Do(true)
.Else(false)
.Result();We have used a Where, but that can be avoided using a wildcard. A wildcard is represented by the Any type and its two (identical) values: any and __. Both are supplied as the preferred value (_) cannot be used as it has special meaning in C# 7 (as a discard) and introducing an Any._ value destroys the ability to use discards. So __ is offered instead, and - for those that find __ ugly, the alternative, any, is also offered.
Using a wildcard, we can change the above function to:
public static bool IsFemale((Animal, Gender) details) =>
details.Match().To<bool>()
.With(__, Gender.Female).Do(true)
.Else(false)
.Result();Action/FuncconversionsCyclemethods- Converting between
ActionandFunc - Extension methods for existing types that use
Option<T> - Indexed enumerations
IEnumerable<T>cons- Option-based parsers
- Partial function applications
- Pattern matching
- Pipe Operators
- Typed lambdas
AnyEither<TLeft,TRight>NoneOption<T>Success<T>Union<T1,T2>Union<T1,T2,T3>Union<T1,T2,T3,T4>UnitValueOrError