Pattern Matching
Pattern matching is a mechanism for checking a value against a pattern. It can be used within Flex transforms and functions to match input values to output values.
Syntax
A match expression begins with the match
keyword, followed by case clauses. The pattern is matched on EXP_MATCH.
flex
match(EXP_MATCH) {PATTERN_1 => EXPRESSION_1;PATTERN_2 => EXPRESSION_2;...PATTERN_N => EXPRESSION_N;};
For example, if v
is an integer value, you can define cases to return a string value like so:
flex
match(v) {0 => "zero";1 => "one";_ => "other";};
Match expressions return a value. You can assign the result to a variable using let
within a function or transform.
flex
function IntToString(v : int32) -> string {let result = match(v) {0 => "zero";1 => "one";_ => "other";};result;}
All pattern matches must be total. Unmatched values can be accounted for in the final case by using either a discard or a bind.
Discard
In the above code, the final case _ => "other"
is a discard. This acts as a catch all for any other possible value. When you use _ =>
you are discarding the value, so it's not available for the case's scope (the code following the =>).
Bind
It is also valid for the final case to be a => "other"
, which is a bind. This also acts as a catch all for any other possible value, and it binds the value to a new variable a
for the case's scope (the code following the =>). The following code is an example using a bind:
flex
match(v) {0 => "zero";1 => "one";a => "other";};
Some and None
There are Some and None constructors available for handling optional types like Optional<float64>
, which may contain a value or nothing. These are used to match on an option with a value vs. without a value.
some(...)
is the constructor for an option that you use when a value is present.none
is the constructor you use when no value is present.
In the below code, if any float64 value is present then its floor value is returned, and if no value exists then 0 is returned.
flex
function OptFloor(f: Optional<float64>) -> int64 {match(floor(f)) {some(n) => n;none => 0;};}
When you use the some(x)
constructor, the x
inside the some constructor is itself a bind. However it is not limited to just a simple bind, in reality the x
can be substituted for any pattern that matches your type.
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 {let sensor_value =match(m.MessageData) {some(msgdata) =>match(msgdata.Reading) {some(reading) => reading.Value;none => 0;};none => 0;};sensor_value;}
Conditional Statements
If/else conditional statements can be incorporated into a match expression as well. In the following code, the value is tested against the pattern and the catch all case includes a conditional statement. For all values that are not 0 or 1, the result will be "positive" if greater than 0 or "negative" if else.
flex
function IntToString2(i : int32) -> string {let result = match(i) {0 => "zero";1 => "one";_ => if i > 0 "positive" else "negative";};result;}
Literals
The following code matches on a literal string value:
flex
match(name: Optional<string>) {some("John") => "Hello John";some("Jane") => "Hello Jane";some(unknown) => "I'm sorry I do not recognize your name " ++ unknown;none => "A nameless stranger";}
Nested Optional
The following code matches on a nested optional:
flex
match(foo: Optional<Optional<int32>>) {some(some(42) => "The meaning of life";some(some(_)) => "A worthless value";none => => "No value provided";}
Arrays
The following code matches on an array:
flex
match(xs: int32[]) {[x, y, z] => x + y + z; // Array has 3 elements[x, y] => x + y; // Array has 2 elements[x] => x; // Array has 1 element[] => 0; // Array has no elements_ => 0; // Array has more than 3 elements
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
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) {RGB(r,g,b) => ...;RGBA(r,g,b,_) => ...;HEX(hex) => ...;}
Polymorphic Types
For polymorphic types – types that are extended by other types – you can pattern match over the parent/super type to determine what the underlying child type is:
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";};
Enum to Enum
The following code show a transform that matches an enum to another enum with the same number of fields.
flex
// Data Types from Message Set 1enum StateEnum1 int32 {DISABLED = 0;ENABLED = 1;ACTIVE = 2;FAILED = 3;}message struct Msg1 {SystemState: StateEnum1;}// Data Types from Message Set 2enum StateEnum2 int32 {PENDING = 0;AVAILABLE = 1;UNAVAILABLE = 2;ERROR = 3;}message struct Msg2 {SystemState: StateEnum2;}// Transform Msg1 to Msg2 by matching enumstransform Msg1toMsg2(i : Msg1) -> Msg2 {let newStateEnum = match(i.SystemState) {StateEnum1.DISABLED => StateEnum2.UNAVAILABLE;StateEnum1.ENABLED => StateEnum2.AVAILABLE;StateEnum1.ACTIVE => StateEnum2.PENDING;StateEnum1.FAILED => StateEnum2.ERROR;};Msg2 {SystemState = newStateEnum;};}
Enum with Discard/Catch All
The following code shows a transform that matches two enums with a different number of fields. The last match case _
is a discard catch all for any other possible values.
flex
// Data Types from Message Set 3enum StateEnum3 int32 {OFF = 0;ON = 1;CYCLE = 2;}message struct Msg3 {SystemState: StateEnum3;}// Data Types from Message Set 4enum StateEnum4 int32 {UNAVAILABLE = 0;AVAILABLE = 1;}message struct Msg4 {SystemState: StateEnum4;}// Transform Msg3 to Msg4 by matching enumstransform Msg3toMsg4(i : Msg3) -> Msg4 {let newStateEnum = match(i.SystemState) {StateEnum3.OFF => StateEnum4.UNAVAILABLE;StateEnum3.ON => StateEnum4.AVAILABLE;_ => StateEnum4.UNAVAILABLE;};Msg4 {SystemState = newStateEnum;};}
Enum to Struct
The following code shows two transform examples involving pattern matching. One where you test a value against all possible values declared in an enum and output a struct value. The other tests against all possible values declared in a struct and outputs an enum value.
flex
// Data Types from Message Set 1enum ColorEnum int32 {Red = 0;White = 1;Blue = 2;}message struct Msg1 {Color: ColorEnum;}// Data Types from Message Set 2struct ColorRGB {R: int32;G: int32;B: int32;}message struct Msg2 {Color: ColorRGB;}// Transform Msg1 to Msg2 by matching enum to structtransform Msg1toMsg2(i : Msg1) -> Msg2 {let newColor = match(i.Color) {ColorEnum.Red => ColorRGB {R = 255; G = 0; B = 0;};ColorEnum.White => ColorRGB {R = 255; G = 255; B = 255;};ColorEnum.Blue => ColorRGB {R = 0; G = 0; B = 255;};};Msg2 {Color = newColor;};}// Other direction: Transform Msg2 to Msg1 by matching struct to enumtransform Msg2toMsg1(i : Msg2) -> Msg1 {let newColor = match((i.Color.R, i.Color.G, i.Color.B)) {(255, 0, 0) => ColorEnum.Red;(255, 255, 255) => ColorEnum.White;(0, 0, 255) => ColorEnum.Blue;(_, _, _ ) => ColorEnum.Blue;};Msg1 {Color = newColor;};}
Primitives
Primitive types (floats, integers, strings, bits) can also be used in pattern matching expressions.
flex
function IntToString(i : int32) -> string {let result = match(i) {0 => "zero";1 => "one";_ => "other";};result;}