Pattern matching

Pattern matching is a language feature that makes it easy to both test and decompose structured data into its constituent parts. While most programming languages provide familiar ways to build structured data, pattern matching enables you to take apart structured data and bring its fragments into scope by binding them to the names you specify. Syntactically, the patterns resemble the construction of structured data, but generally appear in input-direction positions, such as in function argument positions, after the case keyword in switch expressions, and after let or var declarations.

Consider the following function call:

let name : Text = fullName({ first = "Jane"; mid = "M"; last = "Doe" });

We see that an object with three fields is being built, then passed to the function fullName, and the returned result is being captured and brought into scope by binding it to the name identifier. The latter step is referred to as pattern matching, and name : Text is a pattern. When we consider the callee, we’ll encounter something like the following:

func fullName({ first : Text; mid : Text; last : Text }) : Text {
  ...
};

The input is an (anonymous) object, which is deconstructed into its three Text fields, whose values are bound to the identifiers first, mid and last. They can be freely used in the block that forms the body of the function. Above we have resorted to name punning for object field patterns, their general form allows the binding identifier to be distinct from the field name, as in mid = middle_name : Text.

You can also use pattern matching to search for literal patterns, which look just like literal constants. Literal patterns are especially useful in switch expressions because they can cause the current pattern match to fail, and thus start to match the next pattern. For example:

switch ("Adrienne", #female) {
  case (name, #female) { "It's a girl! " # name };
  case (name, _) { name # ", a boy!" };
}

will match the first case clause (because binding to the identifier name cannot fail and the shorthand variant literal #Female compares as equal), and evaluate to "It’s a girl! Adrienne". The second clause showcases the wildcard pattern _. It cannot fail, but won’t bind any identifier.

The last kind of pattern is the or-pattern. As their name suggests, these are two or more patterns that are separated by the keyword or. Each of the sub-patterns must bind to the same set of identifiers, and is matched from left-to-right. An or-pattern fails when its rightmost sub-pattern fails.

Table 1. The following table summarises the different ways of pattern matching.
pattern kind examples appears in can fail remarks

literal

null, 42, (), "Hi"

everywhere

when the type has more than one value

named

age, x

everywhere

no

introduces identifiers into a new scope

wildcard

_

everywhere

no

typed

age : Nat

everywhere

depends

option

?0, ?val

everywhere

yes

tuple

( component0, component1, …​ )

everywhere

depends

must have at least two components

object

{ fieldA; fieldB; …​ }

everywhere

depends

allowed to mention a subset of fields

field

age, count = 0

object

depends

age is short for age = age

variant

#celsius deg, #sunday

everywhere

yes

#sunday is short form for #sunday ()

alternative (or-pattern)

0 or 1

everywhere

depends

no alternative may bind an identifier

Additional information about about patterns

Since pattern matching has a rich history and interesting mechanics, a few additional comments are justified.

  • nomenclature The (usually structured) expression that is being matched is frequently called the scrutinee and the patterns appearing behind the keyword case are the alternatives. When every possible scutinee is matched by (at least one) alternative, then we say that the scrutinee is covered. The patterns are tried in top-down fashion and thus in case of overlapping patterns the one higher-up is selected. A scrutinee is considered dead (or inactive), if for every value that it matches there is higher-up alternative that is also matched.

  • booleans The data type Bool can be regarded as two disjointed altenatives (true and false) and Motoko’s built-in if construct will eliminate the data and turn it into control flow. if expressions are a form of pattern matching that abbreviates the general switch expression for the special case of boolean scrutinees.

  • variant patterns Motoko’s variant types are a form of disjoint union (sometimes also called a sum type). A value of variant type always has exactly one discriminator and a payload which can vary from discriminator to discriminator. When matching a variant pattern with a variant value, the discriminators must be the same (in order to select the alternative) and if so, the payload gets exposed for further matching.

  • enumerated types Other programming languages—for example C, but not Motoko—often use a keyword enum to introduce enumerations. These are impoverished relatives of Motoko’s variant types, as the alternatives are not allowed to carry any payload. Correspondingly, in those languages the switch-like statements lack the full power of pattern matching. Motoko provides the short-hand syntax (as in type Weekday = { #mon; #tue; …​ }) to define enumerations, where payload for the variants is not desired.

  • error handling Error handling can be considered a use-case for pattern matching. When a function returns a value that has an alternative for success and one for failure (for example, an option value or a variant), pattern matching can be used to distinguish between the two as discussed in Errors and optional results.

  • non-failable matching Some types admit only a single value and we call these singleton types. Examples of these are the unit type (also known as an empty tuple) or tuples that only contain singleton types. Variants with only one alternative and no payload (or singleton-typed payload) are singleton types too. Pattern matching on singleton types is particularly straightforward, as it only has one possible alternative, which can never fail.

  • exhaustiveness (coverage) checking When a pattern check alternative has the potential to fail, then it becomes important to find out whether the whole switch expression can fail. If this can happen the execution of the program can trap for certain inputs, posing an operational threat. To this end, the compiler checks for the exhaustiveness of pattern matching by keeping track of the covered shape of the scrutinee. The compiler issues a warning for any non-covered scrutinees (Motoko even tries to come up with an example of a scrutinee that is not patched). As by-product of the exhaustiveness check, warnings are also issued for dead alternatives.

In summary, pattern checking is a great tool for several use-cases and enables active assistance from the compiler to the programmer by pointing out which inputs are not exhaustively taken care of. The static compile-time nature of coverage checking reliably rules out runtime failures.