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.