Skip to content

Pattern Matching

Patterns bind names, unpack sequences and keyed values, test constants or runtime types, and try alternatives. Use them in let, bind, for, and lambda parameters. With if let, if bind, or their while forms, a pattern match controls whether the body runs, and match chooses among several bodies by the first pattern that matches.

Binding and Unpacking

let matches arrays and similar sequences by listing multiple names:

let a b = [1, 2]
assert_eq $a 1
assert_eq $b 2

By default, the pattern must exhaustively match the entire structure or an error will result. Use ... to capture surplus items instead. The rest can be unpacked or spread again. Unpacking an iterator doesn't advance it, so only in-memory iterators such as array, tuple, pair, view, split, and dict key/value iterators can be unpacked; map and filter iterators cannot.

let first ...rest = [1, 2, 3, 4]
assert_eq $first 1
assert_eq [...rest] [2, 3, 4]

Specify nothing after ... to simply ignore surplus items:

let first ... = [1, 2, 3, 4]

Match dictionaries and similar keyed values with key patterns:

let :name age: years = {name: "Alice", age: 30}
assert_eq $name "Alice"
assert_eq $years 30

A pattern can mix positional and keyed items; how they match depends on the source. For dictionaries, positional patterns bind incrementing integer keys:

let first :foo = {foo: 42, "ultramarine"}
assert_eq $first "ultramarine"
assert_eq $foo 42

A pattern binds each name at most once, so let a a = [1, 2] is an error. _ is exempt and may appear any number of times.

Positional and Key Rests

* captures only surplus positional items, and ** only surplus key items. Either or both may end a pattern, * first, in place of .... Surplus items of a kind that neither takes are an error, as without a rest:

let first *others **options = {1, 2, 3, color: "red"}
assert_eq $first 1
assert_eq [...others] [2, 3]
assert_eq {...options} {color: "red"}

* or ** alone discards surplus items entirely.

A rest iterates the way it spreads. A * rest, or the rest of an array or other sequence, yields its values. A ** rest, or a ... rest of a dictionary or record, yields its entries as (key, value) tuples, with positional items keyed by index:

let _ *values **options = {1, 2, color: "red"}
for value = values
  assert_eq $value 2
for key value = options
  assert_eq (key, value) (:color:, "red")

(...rest) makes a tuple unless the parentheses also hold a static key, as in (...rest, extra: 1), which makes a record.

Iterating, spreading or unpacking a rest does not consume it, so it can be read repeatedly. The rest of an iterator is the exception: it is what is left of that iterator, and reading it consumes it. A rest reads its source rather than a copy, so changing the source afterwards can change the rest or make reading it raise an error.

bind

bind takes the scrutinee (the value to match) first, followed by the pattern in vertical layout. Use it to lay out longer patterns across several lines.

bind {1, foo: false, 2, bar: nil}
  a b
  :foo :bar
assert_eq $a 1
assert_eq $b 2
assert_eq $foo false
assert_eq $bar nil
Open in playground

Default Values in bind

bind also permits specifying default values for missing items:

bind []
  a = 1
  b = 2
assert_eq $a 1
assert_eq $b 2

bind [false]
  a = 1
  b = 2
assert_eq $a false
assert_eq $b 2

bind {}
  :foo = 42
assert_eq $foo 42

bind {foo: nil}
  :foo = 42
# nil is a present value, not missing
assert_eq $foo nil
Open in playground

Patterns in for

for matches each element against its pattern:

for k v = {name: "Alice", age: 30}
  echo "$k: $v"

for index value = [10, 20, 30].pairs()
  echo "$index: $value"

for :name :age = [{name: "Alice", age: 30}, {name: "Bob", age: 44}]
  echo "$name is $age years old"
Open in playground

Nested Patterns

An item in parentheses matches its value against a pattern of its own:

let a (b c) = [1, [2, 3]]
assert_eq [a, b, c] [1, 2, 3]

let :name address: (:city :zip) = $
  name: Alice
  address:
    city: Springfield
    zip: 12345
assert_eq $city Springfield
Open in playground

A space must separate the parentheses from a preceding name. Within the parentheses, items read as in a parameter list, so they may have defaults, and newlines are only whitespace:

let id (
  host
  port = 80
) = ["web", ["example.com"]]

A parenthesized pattern always unpacks, even with one item. let (x) = [1] binds the item, where let x = [1] binds the array itself. An empty () matches only an empty value, so if let () = items tests for one.

In vertical layout, an indented block after a key nests the same way:

bind {status: 200, headers: {"content-type": "text/plain"}}
  :status
  headers:
    "content-type": type
assert_eq $type "text/plain"
Open in playground

Dash items can nest positional patterns too:

bind [[1, 2]]
  - - first
    - second

A dash always marks one positional item: - x alone unpacks a one-item sequence, and - key: x alone unpacks a sequence containing one keyed value. A lone bare item in a block still matches the whole value.

Annotations go on the names a sub-pattern binds, never on the sub-pattern, and a sub-pattern has no default of its own. Lambda parameters accept sub-patterns too:

let add = (do |(x1 y1) (x2 y2)| [x1 + x2, y1 + y2])

A def's parameters must be names, which its signature can annotate. Unpack them in the body instead.

Conditional Matching

let and bind after if or while make the pattern match the condition. The bindings are in scope for the branch body when the pattern matches, and the else branch runs when it does not.

if let a b = [1, 2]
  echo "matched $a $b"
else
  echo "no match"

bind takes the same vertical layout as its statement form, with do introducing the branch body. Because the pattern is vertical, it also supports default values:

if bind response
  :status
  :body = ""
do
  echo "$status $body"
else
  echo "unexpected shape"

Both forms work with while, in which case the loop ends the first time the pattern fails to match:

let i = 0
while let a b = pairs.get(i)
  echo "$a $b"
  i = (i + 1)

Both forms also work where if appears in vertical layout, building arrays, dictionaries, or argument lists:

let parts = $
  - always
  if let a b = pair
    - $a
    - $b
  else
    - "no pair"

What Counts as a Match

A pattern takes the failure branch when an unpack has too few or too many positional items, a missing or unexpected key, or a type test or constant comparison fails. Other errors propagate as usual. In particular, trying to unpack a value that does not support it is an error:

# Branches: [1, 2] unpacks fine, but not into three elements
if let a b c = [1, 2]
  echo unreachable
else
  echo "wrong arity"

# Raises: an int cannot be unpacked
if let a b = 42
  echo unreachable
else
  echo also-unreachable

A default supplies a missing element, so it turns what would otherwise be a mismatch into a match.

A nested pattern matches only if every level does.

Binding a Single Name

A pattern that is a bare identifier binds the scrutinee itself and branches on its truthiness without unpacking it:

if let value = lookup key
  echo "found $value"
else
  echo "not found"

To match a sequence of exactly one item instead, write the item in parentheses: if let (value) = items.

let / bind with else

A standalone let or bind can run an else block when its pattern does not match. The bindings remain in scope after the statement:

let k v = s.split "="
else throw std.ValueError "expected KEY=VALUE: $s"
echo "$k -> $v"

bind response
  :status
  :body = ""
else
  return nil

Matching follows the same rules as if let and if bind, including the truthiness test for a single name. The new bindings are not in scope inside else; an existing outer name is still visible there.

In a strict file, else is how to destructure a value that may not match.

The else body must diverge: return, throw, break, continue, or call something that does not return. Reaching its end raises std.RuntimeError, naming the let or bind statement. The type checker warns when it finds a path to that end.

Write else on its own line at the statement's indentation, followed by an indented block or a single inline statement. else if is not accepted here; put an if inside an indented body instead. When the RHS of let is an if or match, its else belongs to that construct, and an additional else cannot be attached to the let.

Constant Patterns

A constant in a pattern matches an equal value using == and binds no name. A bare word still binds a name; quote a string to match it.

if let "GET" path = request
  echo $path

if bind response
  status: 200
  :body
do
  echo $body

A lone constant tests the whole value. Parentheses require a sequence with the listed items. Constants also nest in sub-patterns and runtime type tests, such as Array("GET" path).

false and nil match equal values in conditional patterns; the truthiness test for a lone name does not apply to constants.

A mismatch takes a conditional pattern's failure branch. In a plain let, bind, or for, it raises std.TypeError. Constants cannot have annotations or defaults, and cannot be def parameters. Interpolated strings and other expressions that aren't constants are rejected.

Type-Test Patterns

C(pattern) tests whether a value is an instance of C, then matches the inner pattern against the same value. The test runs before any unpacking:

let Int(n) = 42
let Int(x) Str(label) = [1, "first"]
let Point(x: px y: py) = point

A single positional binding captures the whole value: Int(n) binds an integer without unpacking it. To unpack one element explicitly, use Array((item)). Array() tests for an empty array.

A lone binding still captures the whole value when it has a default: Int(n = 0) tests and binds an integer. Its default applies only when an enclosing ? leaves the value absent. This also holds for a lone binding at the top of a let, bind, or match pattern, in an alternative, or in a block without a dash. Explicit parentheses and dash items always unpack, including (n = 0) and - n = 0.

The parentheses must touch the class name. Int(n) is a type test, while Int (n) binds Int and unpacks another value into n. The class may be a dotted runtime name such as std.Int; it takes no type arguments. Checker-only imports, aliases, and protocols cannot supply a runtime class. The class cannot be bound by the same pattern or parameter list, as in let C C(x) = ...; a later pattern may use it.

In vertical patterns, C $ introduces the inner pattern as an indented block:

bind point
  Point $
    x: px
    y: py

This form works in bind and nested vertical patterns. Use C(pattern) in horizontal bindings. Both forms accept annotations and defaults on inner bindings, but neither accepts an annotation or default on the whole type-test pattern. def parameters accept neither form.

A failed class test raises TypeError in an ordinary binding or lambda parameter. In a conditional binding it takes the failure branch. A successful class test can capture a falsy value: if let Bool(value) = false succeeds and binds false. A subsequent inner pattern can still fail independently.

Non-symbol Keys

:key and key: name match symbol keys. External inputs such as decoded JSON will typically have string keys. To match such keys, use a constant expression instead of a bare key:

import json

let payload = json.decode r|
  {"name": "Alice", "age": 30}

let Dict("name": name "age": age) = payload
assert_eq $name "Alice"
assert_eq $age 30
Open in playground

Alternatives

| separates alternative patterns. They are tried in order, and the first that matches binds the names:

def port_of address
  let Int(port) | _ Int(port) = address
  port

assert_eq (port_of 8080) 8080
let address = ["example.com", 443]
assert_eq (port_of address) 443
Open in playground

At the top level of a let, | separates whole item lists, and each alternative matches the value as an entire pattern would: in let Array((n)) | n = value, the first alternative unpacks a one-item array, while the second binds the value itself. Elsewhere, parentheses group alternatives into one item of the enclosing pattern:

if let ("GET" | "HEAD") path = request
  echo $path

A group with | does not unpack, so to match a sequence of one item against alternatives, double the parentheses: ((Array((x)) | x)). Lambda parameters accept only such groups, and def parameters accept no alternatives.

In vertical layout, a block can consist of | alternatives, each followed by a space. An alternative continues on lines indented two columns past its |:

bind {status: 200, headers: {"Content-Type": "text/plain"}}
  :status
  headers:
    | "content-type": type
    | "Content-Type": type
assert_eq $type "text/plain"
Open in playground

A block is either all alternatives or has none. Alternatives within one line of a block need parentheses.

Every alternative must bind the same names, though in any order and any position; _ is exempt. Defaults run once the whole pattern matches, and only those of the alternative that matched:

if bind args
  | :host
    :port = 80
  | host port
do
  echo "$host:$port"

When no alternative matches, a conditional pattern takes its failure branch. In a plain let, bind, or for, the last alternative's mismatch raises as it would alone. An error that is not a mismatch (see What Counts as a Match) propagates without trying later alternatives.

Optional Sub-patterns

? before a sub-pattern allows its item to be absent. When absent, every binding beneath it takes its default, at any depth. Defaults run in item order and may read earlier bindings:

let ?"settings": ("port": port = 80 "next": next = (port + 1)) ... = {}
assert_eq $port 80
assert_eq $next 81

let first ?(second = 2) = [1]
assert_eq $first 1
assert_eq $second 2
Open in playground

When the item is present, its value must match the sub-pattern as usual. A present nil is not absent: unpacking it raises an error, including in a conditional pattern. ?() accepts an absent item or an empty value.

Each ? applies only at its own level. Here k may be absent, but a present k must contain j; write ?j: to make that inner item optional too:

let ?k: (j: (value = 0)) = data

Every binding under ? needs a default. In an alternative pattern, only the first alternative needs defaults: an absent item takes that alternative's defaults without testing or unpacking it.

let ?k: (Int(n = 0) | Str(n)) = {}
assert_eq $n 0
Open in playground

Bindings on the absent path cannot be named rests; an anonymous ... is allowed. ? cannot precede a plain binding or :key shorthand: use a default instead, such as x = 0 or :key = 0. Required positional items must precede optional ones.

In vertical layout, ? followed by a space marks an optional positional sub-pattern, while a touching ? marks the item after it. - ?(...) also marks an optional positional item:

bind data
  - first
  ? - second = 2
    - third = 3
  ?settings:
    :port = 80
    ...

Lambda parameters use the same syntax, making that parameter optional in the lambda's signature:

let port_of = do |?settings: (:port = 80)| port

match

match tries a value against a list of arms in order and runs the body of the first arm that matches. An arm is a pattern followed by do and a body, either on the same line or as an indented block:

def describe value
  match value
    Int(n) do "int $n"
    "GET" | "HEAD" do "read"
    Array(first _) do
      let inner = describe $first
      "array of $inner"
    _ do "other"

assert_eq (describe 3) "int 3"
assert_eq (describe "HEAD") "read"
assert_eq (describe ([1, 2])) "array of int 1"
assert_eq (describe nil) "other"
Open in playground

An arm's pattern reads as a let pattern does, including | between whole item lists, and may have defaults. Its names are in scope only in that arm.

An arm starting with | lays out its alternatives vertically, as in a vertical alternatives block, with do on a line of its own:

def summary response
  match response
    | :status
      :body = ""
    | status body
    do
      "$status $body"

assert_eq (summary {status: 200}) "200 "
assert_eq (summary ([404, "missing"])) "404 missing"
Open in playground

A bare name matches any value, even false or nil, and binds it; the truthiness test of if let does not apply. _ do is a catch-all arm.

A mismatch moves on to the next arm, but other errors propagate, as described in What Counts as a Match. Trying to unpack a value that does not support it raises, so test the type first when the value may not be a sequence or keyed value:

def size value
  match value
    Array(_ _) do 2
    Array((_)) do 1
    _ do 0

assert_eq (size ([1, 2])) 2
assert_eq (size 5) 0
Open in playground

Guards

if in place of do adds a guard, a condition checked once the pattern matches. The guard can use the arm's names, and the body follows as an indented block. When the guard fails, matching continues with the next arm:

def sign n
  match n
    m if (m < 0)
      :negative:
    0 do :zero:
    _ do :positive:

assert_eq (sign (-3)) :negative:
assert_eq (sign 0) :zero:
assert_eq (sign 4) :positive:
Open in playground

A guard may be if let or if bind, which matches a pattern of its own, as in Conditional Matching. Its names are in scope in the body along with the arm's.

else and the Result

else, lined up with match, runs when no arm matches. else if continues as it does after if:

def lookup table key
  match key
    Str(k) if let value = table.get(k)
      "$k is $value"
  else
    "not found"

assert_eq (lookup {"a": 1} "a") "a is 1"
assert_eq (lookup {"a": 1} 4) "not found"
Open in playground

Like if, match can be the right-hand side of let or an assignment, and gives the result of the body that runs. Without else, it gives nil when no arm matches. The value being matched is evaluated once, before the first arm.

The type checker narrows the value by each arm that does not match, so a later arm sees only what remains:

def run step @ ((:cmd:, Str) | (:wait:, Int))
  match step
    (:cmd: line) do echo "run $line"
    _ do
      # step is (:wait:, Int)
      let _ secs = step
      echo "wait $secs"