Pattern Matching
Most programming languages have an if-else
construct for branching on a condition. While Flex does include if-else
, it also includes a more powerful branching mechanism called pattern matching. The match
expression combines conditional branching and destructuring of values into a single construct.
match
Expression Example
Suppose we have a variant representing an angle in either radians or DMS format.
variant Angle {
Radians(float64);
DMS(int32, int32, int32);
}
We want to write a function to convert an Angle
value into a decimal degree value. This can be accomplished with match
:
function Angle2DecDeg(angle: Angle) -> float64 {
match (angle) {
Angle.Radians(r) => r * 180.0 / 3.1415;
Angle.DMS(d, m, s) => d + m/60.0 + s/3600.0;
};
}
The match
expression above means that angle
should first be compared to the pattern Angle.Radians(r)
. If there is a match, then the free identifiers in the pattern (just r
in this case), should be bound to the corresponding values in angle
. The expression to the right of =>
is then evaluated for its value. If the pattern fails to match, the process repeats with the next pattern.
The list of patterns in a match
expression must be exhaustive, meaning that every value of the discriminee's type must be matched by at least one of the patterns. (If more than one pattern matches, the first pattern is chosen.)
match
Syntax and Semantics
A match
expression begins with the match
keyword followed by the discriminee in parentheses and one or more match arms in curly braces.
The discriminee is the expression that is matched against the patterns.
Each match arm has the form pattern => expr;
If pattern
matches the discriminee (and no previous patterns also match), then the match expression evaluates to the result of evaluating expr
.
match (DISCRIMINEE) {
PATTERN_1 => EXPRESSION_1;
PATTERN_2 => EXPRESSION_2;
...
PATTERN_N => EXPRESSION_N;
};
Don't forget that match expressions, like if/else expressions, are not statements. They return a value that should be bound with let
or used in some other way.
Match Patterns
Free Identifiers
The most basic pattern is a free identifier, which matches any value and binds that value to the identifier. Flex does not allow shadowing, so the free identifier should not be the same as another identifier already in scope.
Discard
The discard pattern, denoted with a single underscore _
, is similar to the free identifier. It matches any value, but does not produce any bindings.
Match expressions must be total, so using the discard pattern at the end as a "catch all" is a good way to guarantee totality.
match (my_number) {
0 => "zero";
1 => "one";
_ => "something else";
}
Literals
Literal patterns match a single numeric, string, or bit value and produce no bindings.
match (name) {
"John" => "Hello John";
"Jane" => "Hello Jane";
unknown => "I'm sorry I do not recognize your name " ++ unknown;
}
match (age) {
18 => "You're exactly 18 years old"
_ => "You're not 18 years old"
}
match (hasBrownEyes) {
true => "You have brown eyes"
false => "You don't have brown eyes"
}
Tuples
The following code matches on a tuple:
match (t: (int32, Optional<string>, float32)) {
(42, some(name), fx) => ...;
(y, none, _) => ...; // We don't care about the float in this case
_ => ...; // Handle any cases not caught above
}
Optional Values
There are two patterns for matching Optional
values: some(...)
and none
.
match (safeDivide(x)) {
some(result) => result;
none => 0.0;
}
Variants
The following code matches on a variant:
variant Color {
RGB(uint8, uint8, uint8);
RGBA(uint8, uint8, uint8, uint8);
HEX(int32);
}
match(color: Color) {
Color.RGB(r,g,b) => ...;
Color.RGBA(r,g,b,_) => ...;
Color.HEX(hex) => ...;
}
Enumerations
Enumeration patterns match the values in an enumeration. The syntax is the same as the syntax that denotes a value of an enumeration.
enum SystemState int32 {
DISABLED = 0;
ENABLED = 1;
ACTIVE = 2;
FAILED = 3;
}
match (ss: SystemState) {
SystemState.DISABLED => ...;
SystemState.ENABLED => ...;
SystemState.ACTIVE => ...;
SystemState.FAILED => ...;
}
Arrays
The following code matches on an array:
match(names: string[]) {
[] => "names is empty";
[n] => "names has a single element: " ++ n
["John", _] => "names has two elements. The first is John"
[_, _, _] => "names has three elements"
}
Extensible Structs
The as
pattern is used to match values of an extensible struct type. The pattern subpat as S
matches if the value is an instance of S
or a subtype of S
.
extensible struct A { ... }
struct B extends A { ... }
struct C extends A { ... }
match (a: A) {
b as B => "a b";
c as C => "a c";
_ as A => "a"
};
Nested Pattern Matching
You can nest match expressions to perform multiple levels of pattern matching on a given input. The following code shows a function that matches on two optional types to always return a SensorValue amount.
struct SensorValue {
Amount : int64;
}
struct SensorReading {
Reading : Optional<SensorValue>;
}
message struct MessageReading {
MessageData : Optional<SensorReading>;
}
function getValue(m : MessageReading) -> int64 {
match (m.MessageData) {
some(msgdata) =>
match (msgdata.Reading) {
some(reading) => reading.Value;
none => 0;
};
none => 0;
};
}