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.
flex
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
:
flex
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
.
flex
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.
flex
match (my_number) {0 => "zero";1 => "one";_ => "something else";}
Literals
Literal patterns match a single numeric, string, or bit value and produce no bindings.
flex
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:
flex
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
.
flex
match (safeDivide(x)) {some(result) => result;none => 0.0;}
Variants
The following code matches on a variant:
flex
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.
flex
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:
flex
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
.
flex
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.
flex
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;};}