match looks at a value, finds the first pattern that
fits, and runs that arm. It replaces long chains of
if/else if and, for the closed types like enums, it checks that you've handled every case.
A match has a subject, one or more arms written
pattern -> body, and ends with end:
fn describe(n)
match n
0 -> return "zero"
x when x > 0 -> return "positive"
_ -> return "negative"
end
end
print(describe(0))
print(describe(7))
print(describe(-3))
Run it:
$ scua describe.scua
zero
positive
negative
Arms are tried top to bottom. The first one whose pattern matches wins, so order matters: put specific patterns before general ones.
#Arm bodies: one
statement, or a do … end block
An arm body is a single statement — that's why every example here
uses one return or print. When an arm needs to
do several things, wrap them in a do … end block, which
counts as one statement:
fn tick(state)
let next = state
match state
"idle" -> do
print("waking up")
next = "running"
end
"running" -> do
print("working")
next = "done"
end
_ -> next = state -- a single statement needs no block
end
return next
end
let s = "idle"
for _ in 0:3 do s = tick(s) end
print(`final: {s}`)
$ scua tick.scua
waking up
working
final: done
Without the block, a two-statement arm is a syntax error, because the
second statement reads as the start of the next arm. State machines are
the usual place you'll reach for do … end, since each arm
tends to act and then compute the next state.
#Literals
A literal pattern matches a value equal to it. Numbers, strings,
true, false, and nil all
work:
match status
200 -> print("ok")
404 -> print("not found")
_ -> print("other")
end
#Bindings and the wildcard
A bare name is a binding. It matches anything and names the value
inside the arm. In the first example, x binds the subject
so the guard can test it.
The wildcard _ also matches anything but binds nothing.
Use it for the catch-all arm when you don't need the value.
#Tags: Ok and Error
Ok(x) and Error(e) are the two shapes of a
Result. A tag pattern matches that tag and binds
(or further matches) its payload. The payload can itself be a pattern,
including a literal:
fn parse(r)
match r
Ok(0) -> return "ok, zero"
Ok(n) -> return `ok {n}`
Error(e) -> return `error: {e}`
end
end
print(parse(Ok(0)))
print(parse(Ok(42)))
print(parse(Error("bad input")))
$ scua parse.scua
ok, zero
ok 42
error: bad input
Ok(0) only matches when the payload is exactly
0; Ok(n) catches every other Ok
and binds the payload to n.
#Enum variants
A variant is matched by its qualified name. If the variant carries a payload, destructure it positionally:
enum AIState {
Idle,
Patrol(int),
Chase(target: int, dist: int),
Dead,
}
fn behave(s)
match s
AIState.Idle -> return "wander"
AIState.Patrol(wp) -> return `patrol to {wp}`
AIState.Chase(t, d) -> return `chase #{t} at range {d}`
AIState.Dead -> return "respawn"
end
end
print(behave(AIState.Idle))
print(behave(AIState.Patrol(7)))
print(behave(AIState.Chase(42, 3)))
print(behave(AIState.Dead))
$ scua behave.scua
wander
patrol to 7
chase #42 at range 3
respawn
The field names on Chase are documentation. You match
the payload by position, not by name. See enums
for more.
#Array patterns
An array pattern matches by length and binds the elements.
[a, b] matches an array of exactly two.
[head, ..tail] matches one-or-more, binding the first
element and a fresh slice of the rest. A bare .. matches
the rest without binding anything:
fn summarize(xs)
match xs
[] -> return "nothing"
[only] -> return `just {only}`
[a, b] -> return `a pair: {a} and {b}`
[h, ..t] -> return `{h}, then {t.len()} more`
_ -> return "not a list"
end
end
print(summarize([]))
print(summarize([99]))
print(summarize([1, 2]))
print(summarize([10, 20, 30, 40]))
print(summarize("hello"))
$ scua summarize.scua
nothing
just 99
a pair: 1 and 2
10, then 3 more
not a list
The patterns are 0-based slices. An array pattern also tests that the
subject is an array, so passing the string "hello" falls
through to the wildcard.
#Table and record patterns
A table pattern matches fields by name. {x} is shorthand
for "has a field x, bind it to x."
{key = pattern} matches the field against a nested pattern,
and a literal there refutes the arm if it doesn't match:
fn render(shape)
match shape
{ kind = "circle", r = r } -> return `circle r={r}`
{ kind = "rect", w = w, h = h } -> return `rect {w}x{h}`
{ x, y } -> return `point ({x}, {y})`
_ -> return "unknown shape"
end
end
print(render({ kind = "circle", r = 5 }))
print(render({ kind = "rect", w = 3, h = 4 }))
print(render({ x = 1, y = 2 }))
$ scua render.scua
circle r=5
rect 3x4
point (1, 2)
A table pattern only names the fields it cares about; extra fields on the subject are fine. The same syntax works on records.
Patterns nest freely. A tag whose payload is a table, or an array of tagged values, is just patterns inside patterns:
match ev
Ok({ url = u, code = 200 }) -> return `200 {u}`
Ok({ code = c }) -> return `ok ({c})`
Error([first, ..]) -> return `first error: {first}`
_ -> return "?"
end
#Guards with when
A when clause adds a boolean test to an arm. The arm
matches only if the pattern fits and the guard is true; otherwise
matching falls through to the next arm:
match n
x when x > 0 -> return "positive"
_ -> return "non-positive"
end
#Alternation with |
| lets one arm cover several patterns:
fn category(n)
match n
0 | 1 | 2 -> return "small"
3 | 4 | 5 -> return "medium"
_ -> return "large"
end
end
print(category(1))
print(category(4))
print(category(9))
$ scua category.scua
small
medium
large
The branches of a | can't bind a name, because the
different branches would bind different things. The compiler stops
you:
$ scua altbind.scua
scua: altbind.scua:2: a `|` pattern cannot bind a name — split it into separate arms
#Exhaustiveness
When the subject is a closed enum, the compiler checks that your arms cover every variant. Leave one out and it tells you which:
enum Dir { North, East, South, West }
fn label(d)
match d
Dir.North -> return "up"
Dir.East -> return "right"
Dir.South -> return "down"
end
end
print(label(Dir.North))
$ scua label.scua
scua: label.scua:3: type error: match on Dir is not exhaustive — missing: West
Add the missing arm, or a _ catch-all, and it compiles.
This check is what makes match over an enum safe to
refactor: add a variant and every match that needs updating becomes a
compile error instead of a surprise at runtime.
#When no arm matches at runtime
If the subject isn't a checked enum (a number, a table, an untyped value), the compiler can't prove the match is complete. If none of the arms fit at runtime, the match raises a fault:
fn classify(n)
match n
1 -> return "one"
2 -> return "two"
end
end
print(classify(1))
print(classify(99))
$ scua classify.scua
one
scua: classify.scua:4: match: no arm matched (got 99)
at classify (line 4)
at main (line 8)
For matches on dynamic data, add a _ arm so there's
always a fallback.
#See also
- Enums — the closed types that get exhaustiveness checking.
- Records and gradual types — the shapes that table and record patterns destructure.
- Errors and faults —
Ok/Errorand what a no-match fault is.