-
Notifications
You must be signed in to change notification settings - Fork 9
Pattern Matching
Pattern matching is a powerful feature found in many contemporary functional programming languages. It presents a conscise and clear way of decomposing complex data structures and performing checks on them, akin to what RegExps are for strings.
The match
expression consists of an expression to check and a set of rules, each optionally returning an expression:
var x = rand 1 10
match x with
case 1 then "one"
case 2 then "two"
case 3 then "three"
case _ then "some other number"
Match expressions are evaluated one by one from top to bottom. If any of them matches, the result is returned and no further rules are checked. If no rules match, then the default(T)
value is returned. No exception is thrown.
LENS provides several useful rules for matching the expressions:
Literals can be of any built-in types: int
, string
, bool
, float
, etc. The null
value is also a literal.
The rule matches if the expression equals the specified literal.
Any identifier found in the rule is a name binding. The matched expression is captured into a variable which is available in the expression that is returned by the rule:
match 1 with
case x then fmt "the value is {0}" x
The underscore character (_
) is special: it means we do not care about the actual value of the expression. It can be placed as many times as needed and is commonly used as the 'catch-all' expression.
Name binding can provide an explicit type signature. Then it will only match if the expression is really of the specified type:
var ex = getException ()
match ex with
case x:ArgumentException then "Arguments are invalid"
case x:DivideByZeroException then "Division by zero"
case _ then "Something went wrong"
A numeric value can be checked for being in a range. The range borders are inclusive:
var x = rand 1 10
match x with
case 1..5 then "one, two, three, four or five"
case 6..8 then "six, seven, eight"
case _ then "nine or more"
Tuple can be decomposed into its elements, applying a rule to each item:
let tuple = new (1; 2; "test"; "bla")
match tuple with
case (x; y; str; _) then fmt "{0} = {1}" str (x + y) // test = 3
Arrays and sequences can be split into items:
match array with
case [] then "empty"
case [x] then fmt "one item: {0}" x
case [x; y] then fmt "two items: {0} and {1}" x y
case [_; _; _] then "three items"
case _ then "incountable multitude of items"
One of the elements can be specified with the ...
prefix. This will mean that it matches not just one item, but a whole subsequence containing zero or more items:
fun length:int (array:object[]) ->
match array with
case [] then 0
case [_] then 1
case [_; ...x] then 1 + (length x)
For arrays arrays, IList<T>
and other collections of finite size, the subsequence can be defined at any position in the rule. For IEnumerable<T>
objects, however, the subsequence can only be defined as the last item of the rule.
For IEnumerable<T>
, the type of the subsequence will also be IEnumerable<T>
, for arrays and lists - T[]
.
Records defined in the LENS script can be decomposed into fields:
record Point
X : int
Y : int
fun describe:string (pt:Point) ->
match pt with
case Point(X = 0; Y = 0) then "Zero"
case Point(X = 0) | Point(Y = 0) then "half-zero"
case _ then "Just a point"
Only the values of specified fields are checked. Fields that are not specified will not be checked.
Types with labels can also be checked and decomposed:
type Expr
IntExpr of int
StringExpr of string
AddExpr of Tuple<Expr, Expr>
SubExpr of Tuple<Expr, Expr>
fun describe:string (expr:Expr) ->
match expr with
case IntExpr of x then fmt "Int = {0}" x
case StringExpr of x then fmt "Str = {0}" x
case AddExpr of (x; y) then fmt "Sum = {0}" ((describe x) + (describe y))
case SubExpr of (x; y) then fmt "Subtraction = {0}" ((describe x) - (describe y))
Please note that the type pattern requires the of
keyword and the existance of a tag. For type labels without a tag the name pattern with explicit type specification is used.
To match a dictionary entry, use the special =>
syntax:
match keyValue with
case key => value then fmt "key = {0}; value = {1}" key value
You can test a string against a regular expression encased in #
characters:
match "String" with
case #^[a-z]+$# then 1
case #^[a-z]+$#i then 2
The following regex modifiers are available:
i = RegexOptions.IgnoreCase
m = RegexOptions.Multiline
s = RegexOptions.Singleline
c = RegexOptions.CultureInvariant
A unique feature in LENS regex pattern matching is that named groups are extracted into variables:
match "My name is John" with
case #^My name is (?<name>\w+)$#i then fmt "Hello, {0}" name
// Result: "Hello, John"
By default, the extracted variables are of string
type. However, it's possible to automatically convert extracted values to a primitive type:
match "I have 2 cookies" with
case #^I have (?<count:int>\d+) cookies$# then fmt "Twice as much will be {0}" (count * 2)
// Result: "Twice as much will be 4"
To be able to convert to type T
, it must provide a bool TryParse(string value, out T result)
method. If the TryParse
method returns false, the next rule will be attempted.
Several rules can be stacked up to yield the same expression using the vertical bar:
match number with
case 1 | 2 | 3 then "one, two or three"
case _ then "other number"
If there are any name bindings, they must match exactly in all of the alternatives in both name and type.
Rules can also be augmented by arbitrary expressions which must evaluate to true
for the rule to match. The when
keyword is used:
match x with
case y when y % 2 == 0 then "even"
case _ then "odd"