Step 4. Pattern matching
Why#
match is an if/else that can take data apart and that the compiler checks for completeness. If you forget a variant of an
enum or a type of a union, the program does not build. That is why match is the main way in OxL to work with an
enum, with results that may be an error and with optional values.
Patterns#
enum Cmd {
Move(dx: i32, dy: i32)
Say(text: str)
Quit
}
data User { name: str; age: i32 }
fn classify(n: i32) -> str =>
match n {
0 => "zero"
1..=9 => "digit" // a range
x if x < 0 => "negative" // a guard: a pattern plus a condition
_ => "big" // _ matches everything else
}
fn run(c: Cmd) -> str =>
match c {
Move(0, 0) => "stay" // literals inside a variant
Move(dx, dy) => "move {dx},{dy}"
Say(t) => "say {t}"
Quit => "bye"
}
fn first(xs: List<i32>) -> str =>
match xs {
[] => "empty" // list patterns
[x] => "one {x}"
[x, ..rest] => "{x} and {rest.len()} more"
}
fn main() io {
for n in [0, 5, -3, 100] { print("{classify(n)}") }
for c in [Cmd.Move(0, 0), Cmd.Move(2, 3), Cmd.Say("hi"), Cmd.Quit] { print(run(c)) }
print("{first([])} | {first([7])} | {first([1, 2, 3])}")
let u = User{ name: "Ann", age: 31 }
if let User{ name, age, .. } = u { // if let destructures a data value when the pattern fits
print("{name} is {age}")
}
let (a, _, c) = (1, 2, 3)
print("{a + c}")
}zero digit negative big stay move 2,3 say hi bye empty | one 7 | 1 and 2 more Ann is 31 4
Walkthrough#
- Patterns are tried top to bottom. A literal (
0), a range (1..=9), the wildcard_, a name binding (x) and a guardx if x < 0are all patterns. Move(0, 0)is a variant pattern with literals inside;Move(dx, dy)binds the fields. Patterns can be nested.[],[x],[x, ..rest]are list patterns: empty, one element, "head and tail".if let User{ name, age, .. } = u { … }runs the block only if the pattern matched;..skips the remaining fields.- A
matchmust be exhaustive: a final_branch or a full enumeration of the variants.
Unions and optional values#
The type A | B is a value of one of the types. T? is shorthand for T | none. A union can be taken apart with match and a
type pattern (i32 n) or tested with the is operator: after if r is TooBig the variable r already has type TooBig inside the branch.
type Port = u16 where 1..=65535 // a refined type: the range is checked on creation
@error data TooBig { max: i32 }
fn describe(x: i32 | str | bool) -> str => // a union: a value of one of the types
match x {
i32 n => "int {n}" // a type pattern with a binding
str s => "text {s}"
bool b => "flag {b}"
}
fn limit(n: i32) -> i32 | TooBig {
if n > 100 { fail TooBig{ max: 100 } }
n
}
fn main() io {
print(describe(5))
print(describe("hi"))
print(describe(true))
let r = limit(500)
if r is TooBig { // is narrows the type inside the branch
print("too big, max {r.max}")
}
let maybe: i32? = none
print("{maybe ?? 0} {maybe is none}")
let name: str? = "Ann"
print("{name?.len() ?? 0}") // ?. calls only if there is a value
match Port.from(80) {
Port => print("port ok")
InvalidValue => print("port rejected")
}
match Port.from(0) {
Port => print("port ok")
InvalidValue => print("port rejected")
}
}int 5 text hi flag true too big, max 100 0 true 3 port ok port rejected
What is new here:
type Port = u16 where 1..=65535is a refined type: the value is checked on creation.Port.from(80)returnsPort | InvalidValue, andPort.from(0)isInvalidValue.name?.len()is a safe call: if there is no value the result isnone, and?? 0substitutes zero.- Errors are ordinary types in a union too (
i32 | TooBig); more in the next step.
Common mistake: an incomplete match#
enum Role { User, Support, Admin }
fn label(r: Role) -> str {
match r {
Role.User => "user"
Role.Support => "support"
}
}non-exhaustive
The compiler tells which variant is not covered: non-exhaustive. The fix is to add a branch
Role.Admin => … or _ => …. Prefer explicit branches: then when a new variant is added you get a compile error
instead of silently falling into _. A branch that can never run is flagged as unreachable-arm.
Remember#
matchis an expression and is always exhaustive.- Patterns: literals, ranges, variants,
data, tuples, lists, alternativesA | B, guardsif. isandmatchnarrow the type;?.and??work withT?.
Next: errors.