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:
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.
Specify nothing after ... to simply ignore surplus items:
Match dictionaries and similar keyed values with key patterns:
A pattern can mix positional and keyed items; how they match depends on the source. For dictionaries, positional patterns bind incrementing integer keys:
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
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
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"
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
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:
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"
Dash items can nest positional patterns too:
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:
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.
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:
Both forms work with while, in which case the loop ends the first time the
pattern fails to match:
Both forms also work where if appears in
vertical layout, building arrays, dictionaries, or
argument lists:
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:
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.
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:
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:
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
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
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:
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"
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:
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
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:
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.
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:
Lambda parameters use the same syntax, making that parameter optional in the lambda's signature:
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"
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"
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
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:
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"
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: