Pattern Matching
Pattern matching is one of those features that, once you have it, you wonder how you ever lived without it. It's like a super-powered switch statement that can destructure data and guarantee you handle every case.
Basic Match Expressions
$day = "Monday";
match $day {
"Monday" => echo "Start of the week",
"Friday" => echo "Almost there",
"Saturday" | "Sunday" => echo "Weekend!",
default => echo "Regular day"
}
You can also use match as an expression:
$emoji = match $day {
"Monday" => "😫",
"Friday" => "🎉",
"Saturday" | "Sunday" => "😎",
default => "😐"
};
Exhaustiveness
Here's where match really shines. When you match on an enum, the compiler ensures you handle every variant:
enum Status {
Pending,
Processing,
Complete,
Failed
}
$status Status = get_status();
// This won't compile - missing cases!
match $status {
Pending => queue(),
Complete => finish()
}
// Error: non-exhaustive match, missing: Processing, Failed
This catches bugs at compile time. Add a new variant to the enum? The compiler tells you everywhere you need to update.
Destructuring
Match can pull apart structured data:
// Destructure Result types
$result Result<i32, Error> = do_something();
match $result {
Ok($value) => echo "Got: {$value}",
Err($e) => echo "Failed: {$e}"
}
// Destructure Option types
$maybe ?string = find_user($id);
match $maybe {
Some($name) => echo "Found: {$name}",
None => echo "Not found"
}
// Destructure tuples
$point = (10, 20);
match $point {
(0, 0) => echo "Origin",
(0, $y) => echo "On Y axis at {$y}",
($x, 0) => echo "On X axis at {$x}",
($x, $y) => echo "At ({$x}, {$y})"
}
Guards
Sometimes you need extra conditions beyond just matching the pattern:
match $number {
$n if $n < 0 => echo "Negative",
$n if $n == 0 => echo "Zero",
$n if $n < 10 => echo "Small positive",
$n => echo "Large: {$n}"
}
Guards are checked after the pattern matches, so you can use the bound variables in the condition.
Matching Multiple Patterns
Use | to match any of several patterns:
match $char {
'a' | 'e' | 'i' | 'o' | 'u' => echo "Vowel",
'0'..='9' => echo "Digit",
default => echo "Other"
}
Range Patterns
Match on ranges with ..= (inclusive) or .. (exclusive):
match $score {
0..=59 => "F",
60..=69 => "D",
70..=79 => "C",
80..=89 => "B",
90..=100 => "A",
default => "Invalid"
}
Struct Patterns
Destructure structs right in the match:
type Point {
x i32
y i32
}
$point = Point { x: 3, y: 0 };
match $point {
Point { x: 0, y: 0 } => echo "Origin",
Point { x: 0, y } => echo "On Y axis at {y}",
Point { x, y: 0 } => echo "On X axis at {x}",
Point { x, y } => echo "At ({x}, {y})"
}
You can also use .. to ignore fields you don't care about:
type User {
id i32
name string
email string
created_at DateTime
}
match $user {
User { name: "admin", .. } => echo "Admin user",
User { name, email, .. } => echo "{name} <{email}>"
}
Binding with @
Sometimes you want to match a pattern but also bind the whole value:
match $response {
Ok($value @ 200..=299) => echo "Success: {$value}",
Ok($value @ 400..=499) => echo "Client error: {$value}",
Ok($value @ 500..=599) => echo "Server error: {$value}",
Ok($value) => echo "Other: {$value}",
Err($e) => echo "Failed: {$e}"
}
If Let
For when you only care about one pattern, if let is more concise:
// Instead of this
match $result {
Ok($value) => process($value),
Err(_) => {}
}
// Write this
if let Ok($value) = $result {
process($value);
}
// With else
if let Some($user) = find_user($id) {
greet($user);
} else {
echo "User not found";
}
While Let
Loop as long as a pattern matches:
// Process items from a queue until empty
while let Some($item) = $queue.pop() {
process($item);
}
Match vs If
When should you use match versus if/else?
Use match when:
- Handling all variants of an enum
- Destructuring complex data
- You have more than 2-3 cases
- You want exhaustiveness checking
Use if when:
- Simple boolean conditions
- Guard clauses with early returns
- You only care about one or two cases
Common Patterns
Processing Results
fn fetch_data($url string): !Data {
$response = http_get($url);
match $response {
Ok($data) => Ok(parse($data)),
Err($e) => Err(error.wrap($e, "Failed to fetch"))
}
}
State Machines
enum State {
Idle,
Loading,
Ready { data: Data },
Error { message: string }
}
fn render($state State) {
match $state {
Idle => show_welcome(),
Loading => show_spinner(),
Ready { data } => show_data(data),
Error { message } => show_error(message)
}
}
Command Handling
enum Command {
Quit,
Move { x: i32, y: i32 },
Say { message: string },
Attack { target: string, power: i32 }
}
fn execute($cmd Command) {
match $cmd {
Quit => exit(),
Move { x, y } => player.move_to(x, y),
Say { message } => chat.send(message),
Attack { target, power } => combat.attack(target, power)
}
}
Pattern matching makes your code's structure match the data's structure. Once you get used to it, you'll find yourself reaching for match all the time.