May 2024 · 9 min read

Pattern Matching Deep Dive

Pattern matching is Lateralus's second most important feature after pipelines. Here's how it works inside the compiler.

Basic matching

match value {
    0         => "zero",
    1..=9     => "single digit",
    n if n < 0 => "negative",
    n         => "large: {n}",
}

Destructuring

Match can destructure enums, structs, tuples, and arrays:

match response {
    Ok(User { name, age, .. }) if age >= 18 => {
        println("Welcome, {name}")
    },
    Ok(User { name, .. }) => {
        println("Sorry {name}, 18+ only")
    },
    Err(NotFound(id)) => println("User {id} not found"),
    Err(e) => println("Error: {e}"),
}

Exhaustiveness checking

The compiler checks that every possible value is handled. If you forget a variant, it's a compile error:

enum Color { Red, Green, Blue }

match color {
    Red => "red",
    Green => "green",
    // ERROR: non-exhaustive pattern: Blue not covered
}

The algorithm works by building a "pattern matrix" and checking if every possible input has at least one matching row. This is based on Maranget's algorithm for ML-family languages.

Guard clauses

Guards add boolean conditions to patterns with if. Guards are checked after the structural match succeeds. The compiler warns if guards make a match potentially non-exhaustive.

Integration with pipelines

Match expressions return values, so they work naturally in pipelines:

results
    |> map(|r| match r {
        Ok(v) => v,
        Err(_) => default_value,
    })
    |> filter(|v| v > threshold)

Pattern matching in pipelines

Where pattern matching really shines is inside pipeline stages. The match keyword integrates naturally with |>:

let results = http_responses
    |> map(|resp| match resp.status {
        200..299 => Success(resp.body),
        301 | 302 => Redirect(resp.headers["Location"]),
        401 => AuthRequired(resp.url),
        403 => Forbidden(resp.url),
        404 => NotFound(resp.url),
        500..599 => ServerError(resp.status, resp.body),
        code => Unknown(code),
    })

// Then handle each variant differently
results
    |> filter_map(|r| match r {
        Redirect(url) => Some(url),  // Follow redirects
        _ => None,
    })
    |> flat_map(http_get)

Exhaustiveness checking

The compiler ensures every match is exhaustive. If you add a new variant to an enum, every match expression that handles it must be updated:

enum ScanResult {
    Open(port, service),
    Closed(port),
    Filtered(port),
    OpenFiltered(port),  // New variant added
}

// Compiler error:
fn describe(result: ScanResult) -> String {
    match result {
        Open(p, s) => "Port {p} open: {s}",
        Closed(p)  => "Port {p} closed",
        Filtered(p) => "Port {p} filtered",
        // Error: non-exhaustive pattern
        // Missing: OpenFiltered(port)
    }
}

This catches bugs that would silently fall through in languages with switch/case statements. Every time a scan tool adds a new result type, the compiler tells you exactly which functions need updating.

Guard clauses

Patterns can have guard clauses for complex conditional logic:

fn classify_vulnerability(vuln: Vulnerability) -> Priority {
    match vuln {
        v if v.cvss >= 9.0 && v.exploitable => Priority::Critical,
        v if v.cvss >= 7.0 => Priority::High,
        v if v.cvss >= 4.0 && v.network_exploitable => Priority::High,
        v if v.cvss >= 4.0 => Priority::Medium,
        _ => Priority::Low,
    }
}

Destructuring nested data

Pattern matching can destructure arbitrarily nested structures in a single expression:

// Parse a complex network packet
match packet {
    Packet { 
        ethernet: Ethernet { src, dst, .. },
        ip: Some(Ip { 
            version: 4, 
            src: ip_src, 
            dst: ip_dst,
            protocol: Tcp,
            payload: Some(Tcp { 
                dst_port: 80 | 443,
                flags: TcpFlags { syn: true, ack: false, .. },
                ..
            }),
            ..
        }),
        ..
    } => {
        log("SYN to web port: {ip_src} -> {ip_dst}")
        track_connection(ip_src, ip_dst)
    },
    _ => (), // Ignore non-matching packets
}

This replaces dozens of nested if statements with a single, readable pattern. The compiler verifies that all field names exist and all types match at compile time.

Or-patterns and binding

Multiple patterns can be combined with |, and shared bindings are extracted:

match event {
    KeyPress(Key::Char('q')) | KeyPress(Key::Escape) => quit(),
    KeyPress(Key::Char(c)) if c.is_alphabetic() => search(c),
    MouseClick(x, y) | TouchStart(x, y) => select_at(x, y),
    Resize(w, h) => relayout(w, h),
    _ => (),
}

The x, y variables are bound in both MouseClick and TouchStart patterns. The compiler verifies that the bindings have the same types in all alternatives.

Lateralus is built by bad-antics. Follow development on GitHub or try the playground.