Note: A very basic match capability is implemented, supporting value equality, bound pattern matching and 'else'.
As with if, pattern matching is about deciding which code path to perform. Its approach is to match a single value against multiple patterns. The code block associated with the first matching pattern is performed.
Let's illustrate this with a simple (but mathematically unusual) example:
fn power(n i32, pow i32) i32: match pow: 0: 1 1: n 2: n * n _: -1
This example calculates its value differently based on the value of pow. If pow is zero, we return 1. If pow is 1, we return n. If pow is two, we return n squared. Finally, if pow is any other value, we return -1.
The structure of this form of pattern matching is straightforward:
- Any normal expression can be used after match. This expression's evaluated value is what patterns are matched with. If desired, an assignment expression can be used here to capture this matchee value for later use in matched code blocks.
- Any number of pattern blocks may follow. Each is comprised of a pattern followed by either a block or else a colon and a expression. If any pattern matches, the evaluated value of its block or expression is the match's value
- Using the _ pattern will always match any value. If specified, it should be the last pattern used.
A versatile range of patterns are supported, able to match wholly to specified values, partially, or using a first-class pattern. Pattern matching also supports de-structuring portions of the original value into bound variables.
Whole Value Matches
These patterns involve straightforward comparisons between the matchee value and one or more pattern values.
The simplest form of matching uses a value as the pattern. This can be a constant value (as above) or any expression that evaluates to some value, for example:
match val: anotherval.len: 4 _: 0
The type of the pattern value must match the type of the matchee value. An equivalence match is performed between these values as if using the '==' operator.
Comparison patterns make use of the comparison operators (including !=). The left hand value is not specified, as it is understood to be the match value:
match n: <0: -1 >0: 1 _: 0
The range pattern checks whether the matchee value lies within a specified range. This is specified by placing either the .. or ... range operator between two expressions:
match n: 0 .. 3: 1 // matches 0, 1, or 2 (but not 3) 3 ... 6: 2 // matches 3, 4, 5 or 6 inclusively
Before continuing on to talk about partial pattern matching, let's introduce ways to enhance the match patterns already described as part of a more comprehensive pattern expression.
It is possible to list more than one possible pattern to match, using or between each pattern:
match n: 2 or 3 or 5 or 7: 2 -6 or -1...1 or 6: 1 _: 0
Multiple values to be matched can be specified on the match statement as a comma-separated tuple or with a function that returns multiple values. If a tuple is specified on the match, then all match patterns must be specified as tuples. For a tuple to match, all values must match individually:
match key,value: 0, _: 0 _, 0: 1 _, _: 2
An 'if' clause may be specified following all patterns as an additional guard. The condition expression after the 'if' must evaluate to true in order for the match to take place.
match nbr: 4 if cnt > 0: 0 // matches only if nbr==4 and cnt>0 4: 1 // matches when nbr==4 and cnt<=0 _: 2 // matches when nbr != 4
When dealing with a composite data structure, partial matching supports value or type matching on selected parts of that data. A partial match pattern mimics the type structure of the data we are matching against, specifying match patterns for the value or type of particular substructural elements. It also supports "destructuring", binding variables to these or other substructural elements.
Partial match patterns begin with the is operator. Additional specifics of the pattern vary by type.
Collection partial matches
The simplest partial match checks whether an array (or array reference) begins with certain elements:
match name: is "Judy": judy // Matches if name string begins with "Judy" is "Frank": frank _: noone
Alternatively, one can specify matching patterns for the first few elements:
match numbers: is [1, _]: 1 is [_, 1]: 2 is [<=0, <=0]: 3 _: 4
Struct partial matches
A matching pattern may be created for specific named fields:
match points: is Point[0., _]: 1 is Point[_, 0.]: 2 is Point[<0., <=0.]: 3 _: 4
Any unspecified fields in a struct pattern are assumed to match.
Variant type partial matches
An variant pattern typically just matches against each option's variant type, binding to a variable:
match oddval: imm intval SomeInt: intval imm point PointStruct: i32[point.x * point.y] is None: 0
If desired, pattern matching and binding can also be applied to the internal value(s):
match oddval: imm intval = SomeInt[<0]: -intval imm intval = SomeInt: intval is PointStruct[x=_, y=_]: i32[x * y] None: 0
Whole and partial matching patterns handle many situations, but not all. First-class patterns make it possible to support more sophisticated matching algorithms. Such algorithms are implemented in a callable function or method that returns true for a successful match.
Use of a first-class pattern is signaled using the using keyword. The matchee value is implicitly included as part of the call (essentially, the first parameter for a function and the second for a method call):
match n: using isPrime(): 0 // invokes isPrime(n) using isOdd(): 1 // invokes isOdd(n) _: 2
This technique can be quite powerful, as it makes it possible to support a broad range of complex pattern matching strategies, such as regular expressions, parsing, etc. A first-class pattern can be an object with state that changes as a result of a successful match. As part of the match, data can be extracted from the matchee value and preserved for later use by the match block.
Note: A first class pattern can also be applied to any arbitrary value as part of an if statement:
++nbr if isOdd(n)