Pattern Matching
Pattern matching gives you a clean way to branch on values, unwrap optionals, and use guard clauses — without long if/else chains. It uses the match keyword and produces a value, making it both a control flow tool and an expression.
This page covers:
- Matching on values (strings, ints, bools)
- Variable binding in match arms
- Optional unwrapping (
?Tandnil) - Guard clauses (
ifconditions on arms) - Block bodies for multi-statement arms
- Match as an expression
- Future: type matching, enums, and exhaustiveness checking
Basic Value Matching
The simplest form matches a value against literal patterns:
match status {
"active" -> print("user is active")
"banned" -> print("user is banned")
"pending" -> print("waiting for approval")
_ -> print("unknown status")
}Each arm is a pattern followed by -> and a body. The _ wildcard matches anything and acts as the default case.
This replaces the common if/else chain:
// Before: repetitive, verbose
if status == "active" {
print("user is active")
} else if status == "banned" {
print("user is banned")
} else if status == "pending" {
print("waiting for approval")
} else {
print("unknown status")
}Match is shorter, the intent is clearer, and you never repeat the variable name.
Matching integers
match http_code {
200 -> "OK"
404 -> "Not Found"
500 -> "Internal Server Error"
_ -> "Unknown"
}Matching booleans
match is_admin {
true -> "full access"
false -> "read only"
}Variable Binding
When a pattern is an identifier (not a literal), it binds the matched value to that name for use in the arm body:
match error_code {
0 -> print("success")
code -> print("failed with code: ${code}")
}The arm code -> ... matches any value and makes it available as code. This is useful as a catch-all that still gives you access to the value, unlike _ which discards it.
Match as Expression
Match produces a value. You can assign the result directly to a variable:
label: str = match status {
"active" -> "Active User"
"banned" -> "Banned"
"pending" -> "Pending Review"
_ -> "Unknown"
}
print(label)This works because each arm’s body is an expression that produces a value. All arms must produce the same type.
You can also use match inline:
print(match mode {
"dark" -> "Dark Mode"
"light" -> "Light Mode"
_ -> "System Default"
})Optional Unwrapping
Matching on optional types (?T) is one of the most common uses. The nil pattern matches absence, and a variable pattern binds the unwrapped value:
user: ?Person = find_user(42)
match user {
nil -> print("no user found")
u -> print("found: ${u.name}")
}In the u arm, the value is unwrapped — u is a Person, not ?Person. The match guarantees it’s not nil, so you get a clean, typed binding without manual nil checks.
Compare with today’s approach:
// Current: manual nil check, no unwrapping
if user != nil {
print("found: ${user.name}")
} else {
print("no user found")
}Match makes the unwrapping explicit and safe.
Optional with guard
age: ?int = parse_age(input)
match age {
nil -> print("no age provided")
a if a < 0 -> print("invalid age")
a if a < 18 -> print("minor")
a -> print("adult, age ${a}")
}Guard Clauses
Guards add conditions to a pattern using if. The arm only matches if both the pattern and the guard are satisfied:
match score {
0 -> "no score"
n if n >= 90 -> "excellent"
n if n >= 70 -> "good"
n if n >= 50 -> "pass"
n -> "fail (${n})"
}The variable n is bound first, then the if condition is checked. If the guard fails, matching continues to the next arm.
Guards keep patterns simple — the pattern does the structural matching, the guard handles the logic.
Guards with optionals
match find_temperature(city) {
nil -> "no data"
t if t > 35.0 -> "hot (${t})"
t if t < 0.0 -> "freezing (${t})"
t -> "normal (${t})"
}Block Bodies
For arms that need more than a single expression, use a block body with { }:
match command {
"quit" -> {
save_state()
print("goodbye")
}
"help" -> {
print("Available commands:")
print(" quit - exit the program")
print(" help - show this message")
}
cmd -> print("unknown command: ${cmd}")
}Block bodies follow the same rules as lambda block bodies — use return to produce a value when match is used as an expression:
result: str = match input {
"" -> "empty"
s -> {
trimmed: str = s.trim()
return trimmed.upper()
}
}Future: Type Matching
When working with trait types, you will be able to match on the concrete type using the name: Type syntax — the same syntax Luma uses for declarations:
trait Shape {}
struct Circle {
radius: float
}
struct Rect {
width: float
height: float
}
fn area(shape: Shape) -> float {
return match shape {
c: Circle -> 3.14 * c.radius * c.radius
r: Rect -> r.width * r.height
_ -> 0.0
}
}The pattern c: Circle means “if the value is a Circle, bind it to c.” Inside the arm, c has the full Circle type with access to its fields.
This replaces type-checking chains:
// Before: manual type dispatch
fn area(shape: Shape) -> float {
if shape.is_type("Circle") {
return 3.14 * shape.radius * shape.radius
} else if shape.is_type("Rect") {
return shape.width * shape.height
}
return 0.0
}Type matching with guards (planned)
fn describe(shape: Shape) -> str {
return match shape {
c: Circle if c.radius > 100.0 -> "large circle"
c: Circle -> "circle (r=${c.radius})"
r: Rect if r.width == r.height -> "square (${r.width})"
r: Rect -> "rectangle (${r.width} x ${r.height})"
_ -> "unknown shape"
}
}Rules
A few rules that keep match predictable:
- Arms are checked top to bottom. The first matching arm wins. Put specific patterns before general ones.
_matches anything and discards the value. Use it as a catch-all at the end.- Variable patterns match anything and bind the value.
n -> ...always matches. - Guards are optional.
n if n > 0 -> ...only matches if the guard is true. - All arms must produce the same type when match is used as an expression.
- A
_or variable catch-all is required for non-exhaustive patterns. The compiler warns if a match could fall through with no matching arm.
Arm ordering matters
Because arms are checked in order, put specific cases first:
// Correct: specific before general
match n {
0 -> "zero"
n if n < 0 -> "negative"
n -> "positive"
}
// Wrong: variable arm catches everything, other arms never run
match n {
n -> "always this"
0 -> "never reached"
}Common Patterns
Replacing if/else chains
// Before
greeting: str = "hey"
if hour < 12 {
greeting = "good morning"
} else if hour < 18 {
greeting = "good afternoon"
} else {
greeting = "good evening"
}
// After
greeting: str = match hour {
h if h < 12 -> "good morning"
h if h < 18 -> "good afternoon"
_ -> "good evening"
}Safe optional access
fn user_label(id: int) -> str {
return match find_user(id) {
nil -> "guest"
user -> "${user.name} (${user.role})"
}
}Command dispatch
match args[0] {
"run" -> run_command(args)
"build" -> build_project(args)
"test" -> run_tests(args)
"help" -> print_help()
cmd -> print("unknown command: ${cmd}")
}Error code handling
message: str = match response.status {
200 -> "success"
201 -> "created"
400 -> "bad request"
401 -> "unauthorized"
404 -> "not found"
code if code >= 500 -> "server error (${code})"
code -> "unexpected status: ${code}"
}Quick Reference
Value matching
match value {
literal -> expr // match exact value
name -> expr // bind to variable
_ -> expr // wildcard (catch-all)
}With guards
match value {
n if n > 0 -> "positive"
n if n < 0 -> "negative"
_ -> "zero"
}Optional unwrapping
match optional_value {
nil -> "absent"
val -> "present: ${val}"
}As expression
result: str = match value {
pattern -> expr
_ -> default_expr
}Block bodies
match value {
pattern -> {
statement1
statement2
return expr
}
}Future: Enums
Enums define a fixed set of named variants. Combined with pattern matching, they let you model choices, states, and alternatives as types — and the compiler ensures you handle every case.
Enums are not yet implemented. The design below shows the planned direction.
Simple enums
A basic enum lists its variants:
enum Color {
Red
Green
Blue
}
color: Color = Color.Red
label: str = match color {
Color.Red -> "red"
Color.Green -> "green"
Color.Blue -> "blue"
}With simple enums, every variant is a plain value with no additional data. The compiler knows all possible variants, so it can verify your match is exhaustive — if you forget a case, the compiler tells you:
// Compiler warning: missing case Color.Blue
match color {
Color.Red -> "red"
Color.Green -> "green"
}Enums with associated data
Each variant can carry its own data, making enums a powerful modeling tool:
enum Shape {
Circle { radius: float }
Rect { width: float, height: float }
Point
}Pattern matching can destructure the associated data:
fn area(s: Shape) -> float {
return match s {
Shape.Circle { radius } -> 3.14 * radius * radius
Shape.Rect { width, height } -> width * height
Shape.Point -> 0.0
}
}The destructuring Shape.Circle { radius } extracts the radius field and makes it available in the arm body. This combines type checking, field access, and variable binding in a single pattern.
Modeling optional results
Enums naturally model “one of several outcomes”:
enum Result {
Ok { value: int }
Err { message: str }
}
fn parse(input: str) -> Result {
if input == "" {
return Result.Err { message: "empty input" }
}
return Result.Ok { value: input.to_int().or(0) }
}
match parse(user_input) {
Result.Ok { value } -> print("parsed: ${value}")
Result.Err { message } -> print("error: ${message}")
}Modeling states
Enums are ideal for state machines and workflows:
enum OrderStatus {
Pending
Shipped { tracking: str }
Delivered { date: str }
Cancelled { reason: str }
}
fn status_label(status: OrderStatus) -> str {
return match status {
OrderStatus.Pending -> "Waiting to ship"
OrderStatus.Shipped { tracking } -> "Shipped (${tracking})"
OrderStatus.Delivered { date } -> "Delivered on ${date}"
OrderStatus.Cancelled { reason } -> "Cancelled: ${reason}"
}
}Exhaustiveness
When matching on enums, the compiler verifies that every variant is covered. This is one of the biggest benefits of enums — if you add a new variant later, the compiler points you to every match that needs updating.
No _ needed (or allowed) when all variants are handled:
// All variants covered — no wildcard required
match color {
Color.Red -> "red"
Color.Green -> "green"
Color.Blue -> "blue"
}If you intentionally want to ignore some variants, use _:
match color {
Color.Red -> "primary"
_ -> "not red"
}Future: Struct Field Matching
Matching on specific field values of structs, without enums:
match person {
Person { name: "Alice" } -> "found Alice"
Person { age } if age >= 18 -> "adult: ${age}"
_ -> "someone else"
}This is a convenience for matching struct shapes directly. The pattern Person { name: "Alice" } matches any Person whose name field equals "Alice".
Future: Multiple Patterns Per Arm
Matching several values in a single arm using |:
match day {
"Saturday" | "Sunday" -> "weekend"
_ -> "weekday"
}This avoids duplicating arm bodies for related patterns.
Roadmap
| Feature | Status | Phase |
|---|---|---|
| Value matching (literals, wildcards) | Done | Phase 1 |
| Variable binding | Done | Phase 1 |
Optional unwrapping (nil / value) |
Done | Phase 1 |
Guard clauses (if) |
Done | Phase 1 |
| Match as expression | Done | Phase 1 |
| Block bodies | Done | Phase 1 |
| Type matching (traits) | Planned | Phase 2 |
| Simple enums | Planned | Phase 3 |
| Enums with associated data | Planned | Phase 3 |
| Exhaustiveness checking | Planned | Phase 3 |
| Struct field matching | Planned | Future |
Multiple patterns per arm (|) |
Planned | Future |