Pattern Matching, Maybe, Result & Try
A case expression takes a value and runs the first arm whose pattern matches.
The compiler enforces exhaustiveness — miss a variant of an enum and the
build fails, not a runtime surprise. We saw the literal-pattern shape in
Bindings & Expressions; this chapter goes deeper
into patterns, Maybe<'T>, Result<'T, 'E>, and try.
Binding patterns and guards
Section titled “Binding patterns and guards”A bare identifier in a pattern position binds the matched value to that name.
Add when <cond> to gate an arm on a runtime check:
fn classify(n: Int): String {
case n {
0 -> "zero"
n when n < 0 -> "negative"
n when n > 100 -> "huge"
_ -> "regular"
}
}
fn main(): String {
dbg classify(0)
dbg classify(-5)
dbg classify(500)
dbg classify(42)
}
Destructuring
Section titled “Destructuring”The real power of case is taking apart structured values. Reusing the
Shape enum from Structs, Enums, Distinct Types:
enum Shape {
variant Circle Float
variant Rectangle {width: Float, height: Float}
}
fn area(s: Shape): Float {
case s {
Shape.Circle(r) -> r * r * 3.14159
Shape.Rectangle{width, height} -> width * height
}
}
fn main(): Float {
dbg area(Shape.Circle(3.0))
dbg area(Shape.Rectangle{width: 4.0, height: 5.0})
}
Tuples and structs destructure the same way: (a, b) -> …, Point{x, y} -> ….
Maybe<'T> and Result<'T, 'E>
Section titled “Maybe<'T> and Result<'T, 'E>”The two enums Nomi reaches for whenever an operation can have a “no answer”
case. Maybe<'T> is None | Some('T) — for an absence with no further
explanation. Result<'T, 'E> is Ok('T) | Err('E) — when failure carries a
description. Both types and their bare constructors are in scope by default.
fn lookup(id: Int): Maybe<String> {
case id {
1 -> Some("Alice")
2 -> Some("Bob")
_ -> None
}
}
fn name_for(id: Int): String {
case lookup(id) {
Some(name) -> name
None -> "unknown"
}
}
fn main(): String {
dbg name_for(1)
dbg name_for(99)
}
For a small, one-off match, if can bind a pattern directly. The names from
the pattern are visible only in the success branch:
fn lookup(id: Int): Maybe<String> {
case id {
1 -> Some("Alice")
_ -> None
}
}
fn label_for(id: Int): String {
if Some(name) = lookup(id) {
name
} else {
"unknown"
}
}
fn main(): String {
dbg label_for(1)
dbg label_for(9)
}
The try keyword
Section titled “The try keyword”When a function returns Maybe<'T> or Result<'T, 'E> and you want to
short-circuit on the failure case, prefix the call with try: the wrapped value is
unwrapped on success and the whole function returns immediately on failure.
fn safe_div(a: Int, b: Int): Result<Int, String> {
if b == 0 {
Err("div by zero")
} else {
Ok(a / b)
}
}
fn calculate(x: Int, y: Int): Result<Int, String> {
half = try safe_div(x, 2)
ratio = try safe_div(half, y)
Ok(ratio + 1)
}
fn main(): Result<Int, String> {
dbg calculate(100, 5)
dbg calculate(100, 0)
}
try is lambda-scoped: inside a lambda, try bubbles to the lambda’s own
boundary (like return), not the enclosing function. Nomi has no non-local
returns.
The next chapter — Interfaces & Dispatch — covers
how Nomi composes behavior across types: interface, impl blocks, dispatch
resolution, and the universal Debug that’s been powering dbg for every
value you’ve seen.