Skip to main content

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 1
enum StateEnum1 int32 {
DISABLED = 0;
ENABLED = 1;
ACTIVE = 2;
FAILED = 3;
}
message struct Msg1 {
SystemState: StateEnum1;
}
// Data Types from Message Set 2
enum StateEnum2 int32 {
PENDING = 0;
AVAILABLE = 1;
UNAVAILABLE = 2;
ERROR = 3;
}
message struct Msg2 {
SystemState: StateEnum2;
}
// Transform Msg1 to Msg2 by matching enums
transform 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 3
enum StateEnum3 int32 {
OFF = 0;
ON = 1;
CYCLE = 2;
}
message struct Msg3 {
SystemState: StateEnum3;
}
// Data Types from Message Set 4
enum StateEnum4 int32 {
UNAVAILABLE = 0;
AVAILABLE = 1;
}
message struct Msg4 {
SystemState: StateEnum4;
}
// Transform Msg3 to Msg4 by matching enums
transform 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 1
enum ColorEnum int32 {
Red = 0;
White = 1;
Blue = 2;
}
message struct Msg1 {
Color: ColorEnum;
}
// Data Types from Message Set 2
struct ColorRGB {
R: int32;
G: int32;
B: int32;
}
message struct Msg2 {
Color: ColorRGB;
}
// Transform Msg1 to Msg2 by matching enum to struct
transform 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 enum
transform 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;
}