8. Pattern matching
open option
open result
struct Point
x: i32
y: i32
end
type LookupError
NotFound(i32)
Unknown
end
def show(x: i32, y: i32) -> string
"#{x},#{y}"
end
def log(p: Point) -> string
"point"
end
def classify!(result: Result<Point, LookupError>) -> string [Crash]
match result
Ok(Point(x, y)) if x > 0 -> show(x, y)
Ok(p@_) -> log(p)
Err(NotFound(id)) -> crash!("missing ##{id}")
Err(_) -> crash!("unknown")
end
end
- Arms use
->. No of keyword (Rust style, not Haskell case ... of). - Guards:
pattern if cond -> expr. - As-patterns:
p@Pattern binds the whole and destructures. - Wildcards:
_. - Struct destructuring binds a struct's fields inside a pattern, either positionally in declaration order (
Point(x, y)) or by name (Point(y=b, x=a), where the written order need not match the declaration order). A keyword struct pattern must name every field, exactly as construction does; an unknown, duplicate, or missing field is a compile error, and the keyword form applies only to structs (a sum variant's payload is positional). Both forms nest (Line(a=Point(x, yy), b)), compose with guards and as-patterns, and lower identically on both tiers - the checker reorders a keyword pattern to declaration order so exhaustiveness and binding see the fields by slot. - List patterns:
[a, b, c] matches a 3-element list; [first, rest @ ..] (TBD) for head/tail. - An arm body is exactly one expression - like an inline closure body (§13), and for the same reason: an arm carries no
end of its own, so the next pattern begins exactly where the body stops. A single self-delimiting expression is fine (if/match/loop/try … end); to run a sequence - a binding or side effect, then a result - wrap it in do … end:
open io
open option
def classify!(input: string, target: i32) -> string [io]
match i32.parse(input)
Some(guess) -> do # do … end groups the two statements
io.print!("checking #{guess}")
if guess < target then "low" elif guess > target then "high" else "hit" end
end
None -> "not a number"
end
end
do … end is a block expression: a sequence of expressions evaluated in order whose value and type are the last one's (an empty do end is ()). It is the escape hatch wherever the grammar wants a single expression - a match-arm body, an inline closure body (§13), an =-bound value. It is an ordinary block, scoped exactly like an if/else branch: it reads and assigns bindings in the enclosing scope (it is not a closure - no capture, no isolation).- Exhaustive by default. Every
match must cover all values of the scrutinee: a sum type needs an arm per variant (or a _), bool needs both true and false, and an open type (int, string, List) needs a _. A non-exhaustive match is a compile error - falling off the end would be an unmarked crash, which a total language (§6) forbids. The escape for a branch you have proven unreachable is an explicit _ -> crash!(...). Guarded arms (pattern if cond) never count toward coverage (the guard may be false), so they always need an unguarded fallback. - Exhaustiveness is complete and witness-based. Nested patterns are recognised as covering whenever the value space they leave open is genuinely covered by the other arms -
Some(Red) | Some(Green) | None over Option<Color> (with Color = {Red, Green}) is exhaustive with no catch-all. A non-exhaustive match reports a concrete uncovered example (e.g. Some(Blue), or a deeper Ok(Some(Green))). Coverage only closes over types with a finite, enumerable value space (sums, structs, bool, ()); the open types above (int, string, List, functions, an uninstantiated generic) can never be closed by listing values and always require a _. - Unreachable arms are an error. An arm already fully covered by earlier arms (a duplicate, or anything after a
_) is a compile error. A guarded arm neither shadows a later arm nor is shadowed by earlier ones (its guard may fail). - An
if/elif chain that dispatches one binding is a match - a compile error (H0618). When three or more of a chain's conditions compare the same binding against a constant (a literal, a variant, or a constructor of constants - c == Some(93u8), s == "GET", x == 3), a match on that binding is the intended form: it binds the payload once, a _ arm captures the fallthrough, and coverage is checked. Rewrite if c == A … elif c == B … elif c == C … else … as match c. The lint only fires on == against a match-able constant; a != chain, an or-compound condition, or a comparison against a runtime value (another binding, a call) is left alone. A leading non-== guard (if n < 24u8 … elif n == 24u8 …) becomes a guarded arm (n if n < 24u8 -> …).