Patterns
Pattern matching
Match is an expression. Destructure literals, tuples, enums, and structs — and let the checker prove you handled every case.
In Lira, match is an expression: each arm yields a value, and
the whole match evaluates to the arm that fits. You reach for it
constantly — to branch on a number, pull data out of an enum, or classify
a point by which axis it sits on. And because the checker enforces
exhaustiveness on enums, a match that forgets
a case is a compile error, not a lurking bug.
Literal patterns
The simplest patterns are literals — integers, strings, and booleans.
Arms are tried top to bottom; a wildcard _ is the catch-all
that matches anything left over. Since match is an
expression, you can return it directly.
// `match` is an expression: every arm yields a value, and the
// whole match evaluates to the arm that fits. Here we match on
// integer literals, with a wildcard `_` as the catch-all.
fn grade(score: int) -> string {
return match score {
100 => "perfect",
90 => "A",
80 => "B",
_ => "keep going"
}
}
// String and bool literals match the same way.
fn greet(lang: string) -> string {
return match lang {
"en" => "hello",
"no" => "hei",
_ => "..."
}
}
println(grade(100))
println(grade(80))
println(grade(42))
println(greet("no"))
println(greet("fr"))Bindings and guards
A bare name in a pattern is a binding: it matches anything and
binds the value so you can use it on the right-hand side. Add a
guard — if cond — to refine an arm so it only
fires when the condition holds. Guards are checked in order alongside the
patterns themselves.
// A bare name in a pattern is a binding: it matches anything and
// binds the value, so you can use it on the right-hand side. A guard
// (`if cond`) refines an arm — the arm only fires when the guard holds.
fn classify(n: int) -> string {
return match n {
0 => "zero",
x if x < 0 => "negative",
x if x % 2 == 0 => "even",
x => "odd: ${x}"
}
}
println(classify(0))
println(classify(-7))
println(classify(8))
println(classify(3))Tuple patterns
Tuple patterns destructure positionally, and each element can be a literal, a binding, or another tuple. Mixing literal and binding elements is where they shine — classifying a point by which axis it lands on falls right out.
// Tuple patterns destructure positionally — and the elements can be
// literals, bindings, or nested tuples. Classifying a point by which
// axis it sits on falls out naturally.
fn quadrant(p: (int, int)) -> string {
return match p {
(0, 0) => "origin",
(0, y) => "on the y-axis",
(x, 0) => "on the x-axis",
(x, y) => "at (${x}, ${y})"
}
}
// Nesting works too: a pattern can reach into an inner tuple.
fn label(p: ((int, int), int)) -> string {
return match p {
((0, 0), z) => "axis origin, depth ${z}",
((x, y), 0) => "flat at (${x}, ${y})",
((x, y), z) => "point in space"
}
}
println(quadrant((0, 0)))
println(quadrant((0, 5)))
println(quadrant((3, 4)))
println(label(((0, 0), 9)))
println(label(((1, 2), 0)))
Nested tuple patterns reach into inner tuples in the same arm, with literal
elements at any depth. The compiler emits the equality checks for you, so
(0, 0) really only matches the origin.
Enum and constructor patterns
Constructor patterns match enum variants. Qualify the variant as
Enum::Variant, and bind any payload right inside the pattern —
single values, multiple fields, or none at all.
// Constructor patterns match enum variants. Qualify the variant with
// `Enum::Variant`, and bind any payload right in the pattern.
enum Shape {
Circle(int),
Rect(int, int),
Point
}
fn area(s: Shape) -> int {
return match s {
Shape::Circle(r) => 3 * r * r,
Shape::Rect(w, h) => w * h,
Shape::Point => 0
}
}
println(area(Shape::Circle(10)))
println(area(Shape::Rect(3, 4)))
println(area(Shape::Point))The same shape powers your own Option- or Result-like types. A generic enum carries a payload of any type, and matching binds it:
// A generic enum makes its own Option type. Matching binds the
// payload of `Some`, and the checker forces you to handle `None`.
enum Opt<T> {
Some(T),
None
}
fn unwrap_or(o: Opt<int>, fallback: int) -> int {
return match o {
Opt::Some(v) => v,
Opt::None => fallback
}
}
println(unwrap_or(Opt::Some(42), 0))
println(unwrap_or(Opt::None, -1))
See Types for more on generic enums, optionals
(T?), and Result with the ?
operator.
Struct patterns
Struct patterns destructure named fields. In a let binding you
can pull the fields you want straight into local names — by shorthand
({ x, y }) or by renaming ({ x: ox, y: oy }).
// Struct patterns destructure named fields in a `let` binding: pull
// the fields you want straight out of a value into local names.
struct Point {
x: int,
y: int
}
fn main() {
let p = Point { x: 3, y: 4 }
// Destructure both fields by name.
let { x, y } = p
println("x is ${x}, y is ${y}")
// Shorthand binds each field to a variable of the same name.
let origin = Point { x: 0, y: 0 }
let { x: ox, y: oy } = origin
println("origin: (${ox}, ${oy})")
}
main()Exhaustiveness
Here is the part worth leading with. When you match on an enum, the checker verifies that every variant is handled. Cover all of them — or add a wildcard — and it compiles. Miss one, and compilation stops with a located error before any bytecode is generated.
This match covers all three variants, so it is accepted:
// Matching every variant of an enum satisfies the exhaustiveness
// checker. No wildcard needed — the compiler can see all cases are
// covered, so this compiles and runs.
enum Signal {
Red,
Amber,
Green
}
fn action(s: Signal) -> string {
return match s {
Signal::Red => "stop",
Signal::Amber => "slow",
Signal::Green => "go"
}
}
println(action(Signal::Red))
println(action(Signal::Amber))
println(action(Signal::Green))
Drop the Green arm with no wildcard to fall back on, and the
checker refuses it:
// @expect-error
// This match omits `Signal::Green` and has no wildcard, so the
// checker rejects it at compile time — before any bytecode runs.
enum Signal {
Red,
Amber,
Green
}
fn action(s: Signal) -> string {
return match s {
Signal::Red => "stop",
Signal::Amber => "slow"
}
}
println(action(Signal::Red))$ lira run nonexhaustive.li 11:12: Match on enum 'Signal' is not exhaustive: missing variant(s) Green. Add the missing arm(s) or a wildcard '_' arm.
Two ways to satisfy it: list every variant, or add a _ arm.
The first is usually better — when you later add a fourth signal, the
compiler will walk you to every match that needs updating, instead of
silently funnelling the new case into a wildcard.
Where to go next
Pattern matching pairs naturally with the rest of the language: bind
received values in a select, model errors as
enums and match on them (Types), or start from the
top with the guide. Every snippet on this page is a
real .li file in the repository, verified to run before this
site is built.