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.

Copyright (c) 2025 Ocean Softworks, Sharkk