Chapter 8: Pattern Matching
Beyond Simple Conditionals
You've been using if statements to make decisions. But if is limited-it checks one condition at a time. When you need to handle multiple cases, especially with structured data, you need something more powerful.
Pattern matching is that something.
match value {
pattern1 => action1,
pattern2 => action2,
pattern3 => action3
}The match expression checks value against each pattern in order. When it finds a match, it executes that branch. The compiler ensures you've covered all possibilities.
This isn't just syntax sugar. It's a different way of thinking about control flow.
Matching Result and Option
The most common use of match is with Result and Option:
fn divide(a: int, b: int) -> Result<int, string> {
if (b == 0) {
return Err("Division by zero")
}
return Ok(a / b)
}
fn main() -> int {
var result = divide(10, 2)
match result {
Ok(value) => {
print("Success: ")
print(value)
},
Err(message) => {
print("Error: ")
print(message)
}
}
return 0
}The patterns Ok(value) and Err(message) destructure the Result. If it's Ok, value gets the wrapped integer. If it's Err, message gets the error string.
This is exhaustive-you must handle both cases. The compiler won't let you forget.
Matching Literals
You can match against specific values:
fn describe_number(n: int) -> string {
match n {
0 => return "zero",
1 => return "one",
2 => return "two",
_ => return "many"
}
}
fn main() -> int {
print(describe_number(0)) // "zero"
print(describe_number(5)) // "many"
return 0
}The _ pattern matches anything. It's the catch-all case.
Matching Enums
Enums are types with multiple variants. Result and Option are enums:
enum Status {
PENDING,
RUNNING,
COMPLETE,
FAILED
}
fn handle_status(status: Status) -> int {
match status {
PENDING => {
print("Waiting to start")
},
RUNNING => {
print("In progress")
},
COMPLETE => {
print("Done")
},
FAILED => {
print("Error occurred")
}
}
return 0
}The compiler checks that you've handled all variants. If you add a new variant to the enum, every match expression breaks until you handle it. This is a feature-it prevents bugs.
Enums with Associated Data
Enums can carry data in their variants:
enum Shape {
Circle(float),
Rect(float, float),
Point
}
fn area(s: Shape) -> float {
return match s {
Shape.Circle(r) => r * r * 3.14
Shape.Rect(w, h) => w * h
Shape.Point => 0.0
}
}
fn main() -> int {
var c = Shape.Circle(5.0)
println(area(c).to_string()) // 78.5
return 0
}The Shape.Circle(r) pattern both matches the variant and extracts the data into r.
Destructuring Structs
Match can destructure structs:
struct Point {
x: int,
y: int
}
fn describe_point(p: Point) -> string {
return match p {
Point { x, y } if x == 0 and y == 0 => "origin",
Point { x, y } if x == 0 => "on y-axis",
Point { x, y } if y == 0 => "on x-axis",
Point { x, y } => "at (${x}, ${y})"
}
}
fn main() -> int {
var p1 = Point { x: 0, y: 0 }
var p2 = Point { x: 0, y: 5 }
var p3 = Point { x: 3, y: 4 }
print(describe_point(p1)) // "origin"
print(describe_point(p2)) // "on y-axis"
print(describe_point(p3)) // "at (3, 4)"
return 0
}The pattern Point { x, y } binds the struct's fields to the names x and y. To match specific field values, add a guard (if ...) after the pattern-we'll cover guards in more detail below.
Patterns are checked in order. The first matching pattern wins.
Guards: Adding Conditions
Sometimes you need more than just pattern matching:
fn classify_number(n: int) -> string {
match n {
_ if n < 0 => return "negative",
0 => return "zero",
_ if n < 10 => return "small positive",
_ if n < 100 => return "medium positive",
_ => return "large positive"
}
}The if after a pattern is a guard. The pattern matches only if the guard is true.
Guards let you express complex conditions while keeping the structure of pattern matching.
Matching Nested Data
Sometimes a value wraps another value-a Result containing an Option, for example. Match the outer layer first, then match the inner value inside the arm:
fn process_result(r: Result<Option<int>, string>) -> int {
match r {
Ok(inner) => {
match inner {
Some(value) => {
print("Got value: ")
print(value)
},
None => {
print("Got ok but no value")
}
}
},
Err(message) => {
print("Error: ")
print(message)
}
}
return 0
}The outer match destructures the Result; the Ok(inner) arm binds the wrapped Option, and the inner match destructures that. Each level stays exhaustive, so the compiler still checks that every combination is handled.
Multiple Patterns with |
Match multiple patterns with the same action:
fn is_weekend(day: string) -> bool {
return match day {
"Saturday" | "Sunday" => true,
_ => false
}
}
fn main() -> int {
print(is_weekend("Saturday")) // true
print(is_weekend("Monday")) // false
return 0
}The | means "or". If the value matches any of the patterns, that branch executes.
Exhaustiveness Checking
The compiler ensures you've covered all cases:
enum Color {
RED,
GREEN,
BLUE
}
fn describe_color(c: Color) -> string {
match c {
RED => return "red",
GREEN => return "green"
// Error: missing BLUE case
}
}This is one of pattern matching's killer features. When you add a new enum variant, the compiler tells you every place you need to handle it.
Real-World: HTTP Status Codes
fn handle_http_status(code: int) -> string {
match code {
200 => return "OK",
201 => return "Created",
204 => return "No Content",
400 => return "Bad Request",
401 => return "Unauthorized",
403 => return "Forbidden",
404 => return "Not Found",
500 => return "Internal Server Error",
502 => return "Bad Gateway",
503 => return "Service Unavailable",
_ => return "Unknown status"
}
}Real-World: Command Parser
enum Command {
QUIT,
HELP,
LIST,
GET,
SET,
UNKNOWN
}
fn parse_command(input: string) -> Command {
return match input {
"quit" | "exit" | "q" => QUIT,
"help" | "h" | "?" => HELP,
"list" | "ls" => LIST,
"get" => GET,
"set" => SET,
_ => UNKNOWN
}
}
fn execute_command(cmd: Command) -> int {
match cmd {
QUIT => {
print("Goodbye!")
return 1 // Signal to exit
},
HELP => {
print("Available commands: quit, help, list, get, set")
},
LIST => {
print("Listing items...")
},
GET => {
print("Getting item...")
},
SET => {
print("Setting item...")
},
UNKNOWN => {
print("Unknown command")
}
}
return 0
}
fn main() -> int {
var input = "help"
var cmd = parse_command(input)
var should_exit = execute_command(cmd)
if (should_exit != 0) {
return 0
}
return 0
}When to Use Match vs If
Use match when:
- You're handling enums (Result, Option, custom enums)
- You're destructuring data
- You want exhaustiveness checking
- You have multiple related cases
Use if when:
- You have a simple boolean condition
- You're checking ranges
- The cases aren't related
- You don't need exhaustiveness
Examples:
// Good: match for enums
match result {
Ok(v) => ...,
Err(e) => ...
}
// Good: if for simple conditions
if (x > 0) {
...
}
// Bad: match for simple boolean
match x > 0 {
true => ...,
false => ...
}
// Bad: if for enums (not exhaustive)
if (result == Ok(...)) {
...
}Pattern Matching Best Practices
Order Patterns from Specific to General
// Good: specific cases first
match n {
0 => "zero",
1 => "one",
_ => "other"
}
// Bad: general case first (unreachable code)
match n {
_ => "other",
0 => "zero", // Never reached!
1 => "one"
}Use Descriptive Variable Names
// Good: clear names
match result {
Ok(user) => print("Processing " + user),
Err(error_message) => print("Error: " + error_message)
}
// Bad: unclear names
match result {
Ok(x) => print("Processing " + x),
Err(e) => print("Error: " + e)
}Don't Ignore Errors
// Bad: ignoring error
match result {
Ok(value) => use_value(value),
Err(_) => {} // Silent failure
}
// Good: handle error
match result {
Ok(value) => use_value(value),
Err(message) => {
print("Error: ")
print(message)
}
}What You've Learned
Pattern matching is more than fancy if statements. It's a way to destructure data, handle multiple cases, and get compile-time exhaustiveness checking.
Use it with Result and Option to handle errors safely. Use it with enums to handle all variants. Use it with structs to extract fields.
Guards add conditions to patterns. The | operator matches multiple patterns. Nested matches handle complex data structures one layer at a time.
The compiler ensures you've covered all cases. This prevents bugs at compile time instead of runtime.
Exercises
Option handler: Write a function that takes
Option<int>and returns the value if present, or 0 if none. Use match.Result transformer: Write a function that takes
Result<int, string>and returnsResult<string, string>, converting the success value to a string.Enum matcher: Define an enum for traffic light colors (RED, YELLOW, GREEN). Write a function that returns the next color using match.
Nested data: Write a function that takes
Result<Option<string>, string>and prints different messages for each case.Guard practice: Write a function that classifies integers into ranges (negative, 0-10, 11-100, 100+) using match with guards.
Command parser: Extend the command parser example to handle commands with arguments (e.g., "get key", "set key value").
Exhaustiveness: Define an enum with 4 variants. Write a match that handles only 3. See the compiler error. Then fix it.
Next: Chapter 9: Modules