-
Notifications
You must be signed in to change notification settings - Fork 15
PatternMatchingOtherTypes
This pattern matching guide is split into the following sections:
- Pattern matching on discriminated unions.
- Pattern matching on options
- Pattern matching on Success<T>
- Pattern matching on tuples
- Pattern matching on
ValueOrError
- Type-based pattern matching for all other types
- Value-based pattern matching on all other types - Covered here.
Value-based pattern matching on general types is achieved via a single extension method, Match()
. This 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 F(T valueMatched)
, or T -> void
in functional parlance, 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 specifying 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 an int
and returning a string
:
var result = 100.Match<int, string>()...
By using Match().To<ResultType>()
, this becomes:
var result = 100.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 = {item}.Match().To<{result type}>()
[WithExpression | WhereExpression ]...
[ElseExpression]
.Result();
WithExpression ==>
.With({value})[.Or({value})]... .Do({item} => {result type expression} |
.With({value})[.Or({value})]... .Do({result type expression})
WhereExpression ==>
.Where({item} => {boolean expression}).Do({item} => {result type expression} |
.Where({item} => {boolean expression}).Do({result type expression})
ElseExpression ==>
.Else({item} => {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({value})[.Or({value})]... .Do({item} => {action}
WhereExpression ==>
.Where({item} => {boolean expression}).Do({item} => {action}
ElseExpression ==>
.Else({item} => {action}) |
.IgnoreElse()
To explain the above syntax:
-
{}
denotes a non-literal, eg{void expression}
could be the empty expression,{}
, or something likeConsole.WriteLine("hello")
. - 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 a bool
, as shown in the code examples below:
public static string YesOrNo(bool value)
{
return value.Match().To<string>()
.With(false).Do(x => "No")
.With(true).Do(x => "Yes")
.Result();
}
public static void PrintYesOrNo(bool value)
{
value.Match()
.With(false).Do(x => Console.WriteLine("No"))
.With(true).Do(x => Console.WriteLine("Yes"))
.Exec();
}
In the first case, we use .Match().To<string>()
to specify we will return a string
via a Func<int, string>
function. If .To
is missed off, then nothing is returned and instead an Action<int>
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 string YesOrNoV2(bool value)
{
return value.Match().To<string>()
.With(false).Do(x => "No")
.Else(x => "Yes")
.Result();
}
public static void PrintYesOrNo(bool value)
{
value.Match()
.With(false).Do(x => Console.WriteLine("No"))
.Else(x => Console.WriteLine("Yes"))
.Exec();
}
One further change can be made to the functional example. We are supplying a parameter, x
, which isn't then used. In this case, we can dispense with the lambda and just specify the return value:
public static string YesOrNoV2(bool value)
{
return value.Match().To<string>()
.With(false).Do("No")
.Else("Yes")
.Result();
}
For a boolean, there are only two values, so the pattern is simple. What about if we want to match many values for an int
for example? We have two choices here: Or
and Where
Firstly, using Or
we could write a function to check for example, a single digit odd number like this:
public static bool IsSingleDigitAndOdd(int value)
{
return value.Match().To<string>()
.With(1).Or(3).Or(5).Or(7).Or(9).Do(true)
.Else(false)
.Result();
}
If we simply wanted to check for a single-digit, positive number though, we can use Where
:
public static bool IsSingleDigitAndPositive(int value)
{
return value.Match().To<string>()
.Where(x => x > 0 && x < 10).Do(true)
.Else(false)
.Result();
}
So far, we've only looked at examples with two match patterns. In many cases though, more may be required and the match patterns can overlap. The following function highlights this:
public static string OddOrPositive(int value)
{
return value.Match().To<string>()
.Where(x => x % 2 == 1).Do(i => string.Format("{0} is odd", i))
.Where(x => x > 0).Do(i => string.Format("{0} is positive", i))
.Else(i => string.Format("{0} is neither odd, nor positive"))
.Result();
}
Clearly in this situation, all positive odd integers will match both Where
clauses. The matching mechanism tries each match in the order specified and stops on the first match. So OddOrPositive(1)
will return 1 is odd
, rather than 1 is positive
.
Enums can be matched just like any other value type, for example:
public static void PrintColorName(Color color)
{
color.Match()
.With(Color.Red).Do(x => Console.WriteLine("Red"))
.With(Color.Green).Do(x => Console.WriteLine("Green"))
.With(Color.Blue).Do(x => Console.WriteLine("Blue"))
.Exec();
}
Succinc<T> doesn't do anything clever with its comparisons, it just uses the default equality function for a type. So if a type compares by reference, the pattern will match on reference. So, for the following code:
class Point
{
int X { get; set; }
int Y { get; set; }
}
class Example
{
private p1 = new Point { X = 1, Y = 2 };
private p2 = new Point { X = 1, Y = 2 };
public bool P1P2AreEqual()
{
return p1.Match().To<bool>()
.With(p2).Do(true)
.Else(false)
.Result();
}
}
Calling P1P2AreEqual()
will return false
.
Clearly the reference type example above is highly contrived, as P1P2AreEqual()
is just a long-winded way of doing p1 == p2
. But pattern-matching has real uses too, as the following example shows. This is a translation of an F# example:
public static int Fib(int n)
{
return n.Match().To<int>()
.With(0).Or(1).Do(x => x)
.Else(x => Fib(x - 1) + Fib(x - 2))
.Result();
}
This is a simple, concise method that uses pattern matching and recursion to "solve" the fibonacci sequence.
Action
/Func
conversionsCycle
methods- Converting between
Action
andFunc
- 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
Any
Either<TLeft,TRight>
None
Option<T>
Success<T>
Union<T1,T2>
Union<T1,T2,T3>
Union<T1,T2,T3,T4>
Unit
ValueOrError