The match Control Operator

The match operator allows you to choose an expression to be executed based on whether an input value matches a given pattern, providing a powerful alternative to nested ternary operators.

Overview

The match operator allows you to choose an expression to be executed based on whether an input value matches a given pattern. It can be very useful when you need to execute different expressions based on complex conditions on the input. For example, if you wanted to transform a student's score into a letter grade, you would write a series of numeric range patterns with the corresponding expression being the letter grade string. The result of the expression that was executed would then be the result of the whole match expression.

In addition to specifying the types and shape of incoming data, patterns can be used to store parts of the incoming data into variables for use in the expression. The combination of matching multiple complex patterns and extracting elements with a straightforward syntax should make it the preferred control-flow operator over the ternary operator.

Example: Letter Grades

A match consists of an input expression and a body that contains one-or-more "arms". Each arm consists of one-or-more patterns and a corresponding expression to execute. When a match expression is evaluated, the input expression is evaluated and the result is checked against each match arm in order. If the input value does not match any of the patterns in the match arms, the match will return null.

match $score {
  90..=100 => "A",
  80..<90  => "B",
  70..<80  => "C",
  _        => "F"
}

Going through this example line-by-line:

  • The first line, match $score, specifies that we want to perform a match on the value of $score
  • Line 2 is a match arm with an "inclusive" range pattern. It will match values greater than or equal to 90 and less than or equal to 100 and return an 'A' on a match.
  • Lines 3-5 are match arms with "exclusive" range patterns. They will match values that are greater than or equal to their lower bound and less than their upper bound. For example, if a score was 89.5, the result of the expression would be a 'B'.
  • Line 6 uses an underscore to specify a default, "catch-all", pattern. Any values that do not match the earlier patterns will match this one and result in an 'F'.

Comparison with Ternary Operator

The following example computes the letter grade for a score using the ternary operator. Nested ternaries can be hard to read and write correctly as the number of cases grow. They also require a lot of repetition since the value being tested needs to be repeated for each check.

$score >= 90 && $score <= 100 ? "A" :
$score >= 80 && $score <  90 ? "B" :
$score >= 70 && $score <  80 ? "C" :
"F"

Complex Example: Finding Phone Numbers

The following is a more complex example that tries to find the 'Home' phone number for a person in their list of numbers. It uses an array pattern with wildcards (...) to specify that the element we're looking for can be at any index in the array and an object pattern to identify the element where the "type" property has the value "Home".

match $person.numbers {
  [..., { type: "Home", value }, ...] => value,
  _ => null
}

Syntax

A match expression starts with the match keyword, followed by the input value and a block of match "arms". Each arm specifies one or more patterns to match against the input and the expression to execute on a successful match separated by a fat-arrow (=>).

The pattern for a match arm can also have a "guard expression" for cases where the supported set of patterns is not enough to fully test the input value. For example, if you wanted to test that a number was divisible by five, you would write a pattern like:

match $n {
  x if (x % 5 == 0) => "divisible by five",
  _ => "not divisible by five"
}

The full syntax for the match expression is as follows:

match <input-expression> {
  <pattern> [if <guard-expression>] => <expression>,
  <pattern> | <pattern> => <expression>,
  _ => <default-expression>
}

Example with Multiple Patterns:

match $s {
  "foo" | "bar" => "matched foo or bar",
  _ => "something else"
}

Patterns

The patterns passed to the match operator allow you to describe the shape and, optionally, the content of the input values. The match operator will try each pattern in turn until a full match is found. The patterns can be simple constants, like integers, or they can describe an element to be found in an array.

Constants

Known input values can be matched using a constant. The supported constants are as follows:

  • Primitives: true / false / null / NaN
  • Integers: Example: 1
  • Strings: Example: "Testing"
  • Objects: Example: { last_name: "Targaryen" }
  • Arrays: Example: ["abc", "def"]

Binding Variables

Unknown values can be captured into variables for use in the corresponding expression and/or the "guard" expression. Note that variables must be used in the corresponding expression or an error will be raised. If the variable is needed, but is not used in an expression you can prefix it with an underscore (_) to silence the error. Use a single underscore as the pattern for the last arm when you want to provide a default value if none of the other arms matched.

If an exclamation mark (!) is appended to the variable name, it must not be null or an empty string.

Examples of variable binding patterns:

  • value - Captures the input value.
  • [first, second, third] - Captures the elements of an array into the variables: first, second, and third.

Capturing the Full Value:

If you're using a pattern that can match multiple values and you need to use the full input value in the expression, you can prefix the pattern with <variable-name> @. For example:

score @ 90..=100 => "High score(!): " + score

Matching Strings

Unknown string values can be matched using regular expressions or by matching just their prefix or suffix using the following syntax:

  • /<regular-expression>/ - Checks if the input is a string and checks it against the given regular expression pattern.
  • [variable-name]..."<suffix>" - Checks if the input is a string and has the given suffix string. Optionally, the leading part of the string can be captured into a variable for use in a guard or corresponding expression. If an exclamation mark (!) is appended to the variable name, the captured string must not be empty.
  • "<prefix>"...[variable-name] - Checks if the input is a string and has the given prefix string. Optionally, the trailing part of the string can be captured into a variable for use in a guard or corresponding expression. If an exclamation mark (!) is appended to the variable name, the captured string must not be empty.

Examples:

// Match email addresses with regex
match $email {
  /^[^\s@]+@[^\s@]+\.[^\s@]+$/ => "Valid email",
  _ => "Invalid email"
}

// Match file extensions using suffix
match $filename {
  name...".txt" => "Text file: " + name,
  name...".pdf" => "PDF file: " + name,
  _ => "Unknown file type"
}

// Match URL schemes using prefix
match $url {
  "https://"...rest => "Secure: " + rest,
  "http://"...rest => "Insecure: " + rest,
  _ => "Unknown protocol"
}

Number Ranges

Unknown number values can be matched against a range using the following syntax:

  • |lower-bound|..=|upper-bound| - Inclusive upper-bound. This version will match numbers that are greater than or equal to the lower-bound and less-than-or-equal-to the upper-bound.
  • |lower-bound|..<|upper-bound| - Exclusive upper-bound. This version will match numbers that are greater than or equal to the lower-bound and less-than the upper-bound.
  • |lower-bound|.. - Minimum number. This version will match numbers that are greater than or equal to the lower-bound.

The bounding values can only be integers due to the difficulty in comparing decimal numbers correctly. However, the input value can be a decimal. For example, the range 1..<2 will match the integer 1 and the decimal value 1.5.

Examples:

// Temperature ranges
match $temperature {
  ..0 => "Freezing",
  0..<20 => "Cold",
  20..<30 => "Comfortable",
  30.. => "Hot"
}

// Age groups
match $age {
  0..<13 => "Child",
  13..<18 => "Teenager",
  18..<65 => "Adult",
  65.. => "Senior"
}

Objects

Unknown objects can be checked to see if they contain a given set of properties and if their property values match a given pattern. Object patterns are written like object literals where the property values are themselves patterns. The pattern will only match the input if the given properties are found in the input object (except if the '?' flag is used on the property name) and the property value matches the corresponding pattern. If the input object contains properties that are not found in the pattern, they are ignored and the object pattern will still match.

Object Pattern Shortcuts:

  1. Implicit Variable Binding: Property values are optional and the value of the property will be captured in a variable with the same name as the property. For example:
    { first_name, last_name } => first_name + " " + last_name
  2. Required Properties (!): Appending an exclamation mark (!) to a property name marks it as required, meaning its value must not be null or an empty string:
    { last_name!, first_name! } => first_name + " " + last_name
  3. Optional Properties (?): Appending a question mark (?) to a property name marks it as optional, meaning it can be missing from the input object and the value of the variable will be set to null:
    { last_name!, first_name!, nick_name? } => (nick_name || first_name) + " " + last_name

Examples:

// Match user types
match $user {
  { role: "admin", permissions } => "Admin with: " + permissions,
  { role: "user", name! } => "Regular user: " + name,
  _ => "Unknown user type"
}

// Match with nested objects
match $person {
  { address: { city: "San Francisco" }, name } => name + " from SF",
  { address: { city }, name } => name + " from " + city,
  _ => "No address"
}

Variably-Sized Arrays

Arrays of unknown size can be matched and their elements extracted using wildcards (...) in array patterns. When placed at the start of an array pattern, the pattern will scan the array to find an element that matches the next pattern in the array pattern. When placed at the end of an array pattern, the pattern will match any remaining values in the array. To capture the elements that are matched by a wildcard, you can append a variable name to the pattern (e.g. ...tail).

Wildcard Patterns:

  • [head, ...tail] - Captures the first element into the "head" variable and all of the remaining elements into the "tail" variable.
  • [...head, tail] - Captures the last element into the "tail" variable and all of the leading elements into the "head" variable.
  • [...head, "|", ...tail] - Splits an array around the pattern in the middle (a string with a pipe symbol in this case) and stores the elements before and after the split into the "head" and "tail" variables, respectively.
  • [..., {type: "Home", value}, ...] - Scans the array to find the element that matches the pattern in the middle. In this example, the element to match has a property named "type" with the value of "Home" and a "value" property that should be captured.
Important: It is an error to place wildcards next to each other since there is no way for the pattern to figure out where one wildcard ends and the next begins. If an array pattern has no wildcards, it will only match input arrays that have the same number of elements as the array pattern.

Examples:

// Head and tail
match $list {
  [first, ...rest] => "First: " + first + ", Rest: " + rest.length + " items",
  [] => "Empty list",
  _ => "Something else"
}

// Find specific element
match $contacts {
  [..., { type: "email", value }, ...] => "Email: " + value,
  [..., { type: "phone", value }, ...] => "Phone: " + value,
  _ => "No contact info"
}

// Split around delimiter
match $path {
  [...dirs, "/", file] => "File: " + file + " in " + dirs.join("/"),
  _ => "Not a path"
}

Combining Patterns

Unknown input values can be matched against multiple patterns by joining the patterns with a pipe symbol (|).

Example:

"foo" | "bar" => "matched foo or bar"

If you need to capture the input value to use in the expression, you can prefix the pattern with the syntax <variable-name> @, like so:

s @ ("foo" | "bar") => "matched: " + s

More Examples:

// Match multiple status codes
match $statusCode {
  200 | 201 | 204 => "Success",
  400 | 401 | 403 | 404 => "Client error",
  500 | 502 | 503 => "Server error",
  code @ _ => "Unknown code: " + code
}

// Match file types
match $extension {
  "jpg" | "jpeg" | "png" | "gif" => "Image",
  "mp4" | "avi" | "mov" => "Video",
  "mp3" | "wav" | "flac" => "Audio",
  ext @ _ => "Unknown: " + ext
}

Practical Examples

Processing API Responses:

match $response {
  { status: 200, data! } => data,
  { status: 404 } => null,
  { status, error } => "Error " + status + ": " + error,
  _ => "Invalid response"
}

Routing Based on Path:

match $request.path {
  "/api"...rest => handleApiRequest(rest),
  "/admin"...rest => handleAdminRequest(rest),
  "/static"...rest => handleStaticFile(rest),
  _ => handleNotFound()
}

Validating Input:

match $input {
  { email!, password! } if (password.length >= 8) => "Valid",
  { email!, password } => "Password too short",
  { email } => "Password required",
  _ => "Email required"
}

Processing Array Data:

match $data {
  [] => "No data",
  [single] => "Single item: " + single,
  [first, second] => "Two items: " + first + ", " + second,
  [first, ...rest] => "Multiple items, first: " + first,
  _ => "Not an array"
}

Best Practices

  • Always Include Default: Always include a catch-all pattern (_) as the last arm to handle unexpected cases.
  • Order Matters: Place more specific patterns before more general ones, as the first matching pattern wins.
  • Use Guards Sparingly: Prefer pattern matching over guard expressions when possible for better readability.
  • Variable Naming: Use descriptive variable names in patterns to make the code self-documenting.
  • Required vs Optional: Use ! for required non-null properties and ? for optional properties.
  • Prefer match Over Ternary: Use match instead of nested ternary operators for better readability and maintainability.
  • Extract Variables: Use the @ syntax to capture the full value when needed.
  • Wildcard Limits: Don't place wildcards next to each other in array patterns.

Advantages Over Ternary Operators

  • Readability: Match expressions are easier to read and understand than nested ternaries.
  • No Repetition: The input value is specified once, not repeated for each condition.
  • Pattern Matching: Support for complex patterns like ranges, objects, and arrays.
  • Variable Extraction: Ability to extract parts of the input into variables.
  • Type Safety: Patterns can validate the shape and type of data.
  • Maintainability: Easier to add new cases or modify existing ones.