Skip to content

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.

wyn
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:

wyn
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:

wyn
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:

wyn
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:

wyn
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:

wyn
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:

wyn
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:

wyn
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:

wyn
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:

wyn
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

wyn
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

wyn
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:

wyn
// 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

wyn
// 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

wyn
// 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

wyn
// 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

  1. Option handler: Write a function that takes Option<int> and returns the value if present, or 0 if none. Use match.

  2. Result transformer: Write a function that takes Result<int, string> and returns Result<string, string>, converting the success value to a string.

  3. Enum matcher: Define an enum for traffic light colors (RED, YELLOW, GREEN). Write a function that returns the next color using match.

  4. Nested data: Write a function that takes Result<Option<string>, string> and prints different messages for each case.

  5. Guard practice: Write a function that classifies integers into ranges (negative, 0-10, 11-100, 100+) using match with guards.

  6. Command parser: Extend the command parser example to handle commands with arguments (e.g., "get key", "set key value").

  7. 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

MIT License - v1.19.1