Skip to main content

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;
    };
}