What is Pattern Matching?
Pattern matching is an essential and powerful building block to many functional programming languages like Haskell or Scala. Pattern matching allows the developer to match a value (or an object) against some patterns to select a branch/block of the code.
I have used the Type Pattern to demonstrate the concept of the pattern matching
As you can see above, the type pattern can be used as a replacement for type check and type cast. You can consider pattern matching as a replacement for if-else and the classic switch cases. But in the end, it is still another type of if-else.
Pattern Matching Core Concept
Consider the following example,
public class Car { private int fuel; private int speed; public Car(int fuel, int speed) { this.fuel = fuel; this.speed = speed; } public void Deconstruct(out int fuel, out int speed) { fuel = this.fuel; speed = this.speed; } public int GetFuel() { return this.fuel; } public int GetSpeed() { return this.speed; } public void Drive() { // Bohn Bohn brunn brunn this.speed = this.speed + 10; this.fuel = this.fuel - 5; } }
The object in the Object-Oriented Programming is encapsulated in one unit (the state and the behavior), the data structures, and the methods which work on those data structures. If you need new functionality, then we have to add a new method to the object definition. For example, you need to stop the car, then you have to add the Stop() {} to the class Car. If you want to add a few methods, then this approach is working fine. The problem comes when you want to add continually new methods all the time. You might realize that you have already written dozens of methods, and you have to add more methods like EngineOn(){}, Break(){}, and TurnLightOn(){}… etc. That can be quite a challenge to keep the code still maintainable. Pattern matching provides an alternative way to handle those problems.
The idea is simple.
Constructor In the constructor, you pass the data structure (parameters) to collect and create the object.
public Car(int fuel, int speed)
Deconstruct Destructs an object to a tuple. It takes the data structures out of the object.
public void Deconstruct(out int fuel, out int speed)
Figure -2- OOP vs. FP As shown, in the image above, it becomes easy to perform the Car operations on the deconstructed data structures, and you can add the new operations(methods) easily. The core concept is, when you have a stable and a known data structure, it’s often very interesting to apply the pattern matching approach because you can quickly expand the operations. However, if your operations are stable, but the data changes, then the OOP approach seems more adequate.
Why is Pattern Matching so important in C#?
The first reason is that functional programming can be complementary to the OOP paradigm, and in recent history, we have seen many successful technologies based on the combination between OOP and FP like LINQ. The second reason is more political. C# is one of the top 10 languages. The world is changing very fast, and therefore, functional programming over time becomes more important than any other approach. The .Net development team could not tolerate the idea of losing their success; for this reason, they have decided to work aggressively in the innovation world. They are trying to expand everywhere, adding more new features and programming paradigms, and they are trying to make C# more performant and simplifying the syntax. I was not surprised when I saw the C# development team brings pattern matching and records to the center of the C# programming style. I suppose, with C# 10, we can do everything in a functional way. This aggressive development strategy is good and evil. One thing I have noticed in the last few years is that many C# developers are angry about that. Furthermore, the F# developers are very disappointed about including FP in C# and not empowering F#. At this moment, F# evangelism and F# prophets became hopeless, and they are trying to compare the language syntaxes, and they are assuming that F# concepts deserve better.
Recap
C# 7.x
- Constant patterns
- Type patterns
- Var patterns
- Destruction
- Pattern matching on generic type parameters
C# 8
- Pattern matching enhancements:
- Switch expressions
- Property patterns
- Tuple patterns
- Positional patterns
C# 9 probably
- Type patterns
- Parenthesized patterns
- Relational patterns
- Combinator Patterns
- Conjunctive and patterns that require both of two different patterns to match.
- Disjunctive or patterns that require either of two different patterns to match.
- Negated not patterns that require a given pattern not to match.
I recommend you to read my old article about Pattern Matching: https://www.infoq.com/articles/cs8-ranges-and-recursive-patterns/
!!I MPORTANT !!!
The plan and the C# 9 proposals, still in the draft stage, have not yet been put into the final form for a decision by the .Net development team. The syntax might be changed, or the below proposal might be removed from the C# 9 milestone.
C# 9 Pattern Matching
The .NET development team is considering a small handful of enhancements to pattern matching for C# 9.0 that have a natural synergy and work well to address several common programming problems,
- Type patterns is used to match the input against a type. If the input type is a match to the type specified in the pattern, the match succeeds.
- Parenthesized patterns permit the programmer to put parentheses around any pattern.
- Relational patterns permits the programmer to express that an input value must satisfy a relational constraint when compared to a constant value.
- Combinator Patterns permits the programmer to combine multiple patterns on one line, with AND/OR operators, or to negate a pattern by using the NOT operator:
- Conjunctive and patterns that require both of two different patterns to match.
- Disjunctive or patterns that require either of two different patterns to match.
- Negated not patterns that require a given pattern not to match.
Type Patterns
In C# 7, we have seen that the is-type operator extends to be an is expression. C# 9 introduces a new type pattern, which can also be used to match the input against a type. This pattern is nothing more than a minor syntactic convenience.
Example:
int age = 42; string name= "Bassam"; var userTuple = (age, name); // Current version // test if age is an int and name is a string if (userTuple is (int _, string _) ) { Console.WriteLine(userTuple.name); } // C# 9 // test if age is an int and name is a string if (userTuple is (int, string) ) { Console.WriteLine(userTuple.name); }
Compare the C# Syntax trees, Current C# version,
Comparing the IL Code for Pattern Matching
Produced IL Code
// int age = 42; IL_0000: ldc.i4.s 42 IL_0002: stloc.0 // string name= "Bassam"; IL_0003: ldstr "Bassam" IL_0008: stloc.1 // var userTuple = (age, name); IL_0009: ldloca.s 2 IL_000b: ldloc.0 IL_000c: ldloc.1 IL_000d: call instance void valuetype [System.Private.CoreLib]System.ValueTuple`2<int32, string>::.ctor(!0, !1) // if (userTuple is (int _, string _) ) // Current Version IL_0012: ldloc.2 IL_0013: ldfld !1 valuetype [System.Private.CoreLib]System.ValueTuple`2<int32, string>::Item2 IL_0018: brfalse.s IL_0025 // System.Console.WriteLine(userTuple.name); IL_001a: ldloc.2 IL_001b: ldfld !1 valuetype [System.Private.CoreLib]System.ValueTuple`2<int32, string>::Item2 IL_0020: call void [System.Console]System.Console::WriteLine(string) // if (userTuple is (int, string) ) // C# 9 IL_0025: ldloc.2 IL_0026: ldfld !1 valuetype [System.Private.CoreLib]System.ValueTuple`2<int32, string>::Item2 IL_002b: brfalse.s IL_0038 // System.Console.WriteLine(userTuple.name); IL_002d: ldloc.2 IL_002e: ldfld !1 valuetype [System.Private.CoreLib]System.ValueTuple`2<int32, string>::Item2 IL_0033: call void [System.Console]System.Console::WriteLine(string)
Pattern Combinators
Permits matching both of two different patterns using and/or operators or the negation of a pattern by using not operator. Pattern Combinators consist of the following sub-patterns,
- Conjunctive and patterns that require both of two different patterns to match.
- Disjunctive or patterns that require either of two different patterns to match.
- Negated not patterns that require a given pattern not to match.
AND “Conjunctive” Pattern
Permits the AND Logics on two different patterns.
Example: if (o is int and 1) { } // The code block will be executed if the o is of type int and its value is 1
OR “Disjunctive” Patter
Permits the OR Logics on two different patterns.
Example: if (o is (1 or 2) and int x4) { } // The code block will executed if the value of o is 1 or 2
Negated Pattern
Permits the NOT Logics on a pattern.
if (e is not null) { }
The code will be executed when e is a reference type and not null. I love It! And I hope the keyword not
will not replace with the ugly operator ~
.
Neal Gafter example:
The and or combinators will be useful for testing ranges of values.
bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
Examples: if (o is int x1 and 1) { } if (o is int x2 and (1 or 2)) { }
The code block will be executed if the o
is of type int
, then (copy o
to x2), and x2
value must be 1 or 2. As we can see above, we have also used Parenthesized patterns. Unfortunately, the Range Pattern is declined; in other words, you cannot do in C# 9: if (o is int x2 and [1..2])
The alternative for that is Relational Patterns, which is described in the section below.
if (o is 1 and int x3) { } if (o is not (1 or 2) and int x5) { }
Parenthesized patterns
According to the documents, the Parenthesized patterns can be grouped around patterns to achieve the desired associativity. In the following example, parentheses are used to control associativity between AND pattern and Or pattern.
Back to the Neal Gafter example:
The and and or combinators will be useful for testing ranges of values.
bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z'; // and & or combinators
The below example illustrates that the and-operator will have a higher parsing priority (i.e. will bind more closely) than the or-opreator . The programmers can use the parenthesized pattern to make the precedence explicit:
bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
Relational patterns
Allows the case-labels in switch-statement to support the comparison operators, much like the Select Case statement in Visual Basic.
Supported operators and types
<=, >, and >= on all of the built-in types that support such binary relational operators with two operands of the same type in an expression. Specifically, it supports all of these relational patterns for sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, and decimal. Additionally, it supports == and != for string and bool.
Back to Neal Gafter Examples:
bool IsHexLetter(char c) => c is in 'A' to 'Z' or in 'a' to 'z';
The to keyword would be used to specify a range. In this case [A..Z] or [a..z].
Switch-label
So, you could write code as in this example:
int iq = DoIqTest(); switch (iq) { case <= 69: ProcedureExtremelyLow(); break; case 70 to 79: BorderlineProcedure(); break; case 80 to 89: LowAverageProcedure(); break; case 90 to 99: case 101 to 109: AverageProcedure(); break; case 100: ExactlyMedianProcedure(); break; case 110 to 119: HighAverageProcedure(); break; case 120 to 129: SuperiorProcedure(); break; case >= 130: VerySuperiorProcedure(); break; }
The to keyword would be used to specify a range. In the statement switch (value), case x to y:
would be equivalent to the boolean expression value >= x && value <= y
.
Summary
The term pattern matching is a central feature of functional programming languages. This approach can be used to solve many programming problems efficiently. C# 9 and 10 will support the most F# Pattern Matching concepts.
742 thoughts on “Intro To Pattern Matching – Covers C# 9”