Mastering Pattern Matching in Rust
In Rust, one of the core features that distinguishes it from many other languages is its powerful system for pattern matching. Pattern…
In Rust, one of the core features that distinguishes it from many other languages is its powerful system for pattern matching. Pattern matching provides a way to access the data in data structures, and it’s a tool that allows for concise and expressive code. In this article, we’ll dive deep into pattern matching in Rust to understand its strengths and intricacies.
What is Pattern Matching?
Pattern matching allows you to compare a value against a pattern and, if they match, to destructure or extract parts of that value. It’s like a Swiss army knife for Rustaceans; whether you’re working with option types, results, enums, or destructuring complex data types, pattern matching comes to the rescue.
Basic Pattern Matching with match
The match
keyword is the heart of pattern matching in Rust. It allows you to compare a value to multiple patterns and execute code based on the first pattern that matches.
let x = Some(5);
match x {
Some(5) => println!("It's a five!"),
Some(_) => println!("It's something, but not a five."),
None => println!("It's nothing."),
}
Here, x
matches the first arm, so "It's a five!" is printed.
Destructuring
Pattern matching shines when you’re dealing with complex data types. For instance, when you’re working with structs or enums, you can use patterns to extract values.
struct Point {
x: i32,
y: i32,
}
let p = Point { x: 1, y: 2 };
match p {
Point { x, y: 2 } => println!("x is {} and y is 2", x),
Point { x: 0, y } => println!("x is 0 and y is {}", y),
_ => println!("Other case"),
}
Using _ as a Wildcard
In patterns, the _
symbol can be used as a wildcard, meaning it will match any value but not bind it to a name.
let x = 3;
match x {
1 => println!("One"),
2 => println!("Two"),
_ => println!("Other"),
}
Using if in Match Arms
Rust allows for even finer control with pattern matching by allowing conditions in match arms with the if
keyword.
let pair = (2, -2);
match pair {
(x, y) if x == y => println!("These are twins"),
(x, y) if x + y == 0 => println!("Antimatter, kaboom!"),
(x, _) if x % 2 == 1 => println!("The first one is odd"),
_ => println!("No correlation..."),
}
Matching on Enums
Rust’s enums are versatile, and when combined with pattern matching, they become even more powerful.
enum Result<T, E> {
Ok(T),
Err(E),
}
let res: Result<i32, &str> = Result::Ok(42);
match res {
Result::Ok(value) => println!("Success: {}", value),
Result::Err(err) => println!("Error: {}", err),
}
Advanced Pattern Matching
Beyond the basics, Rust’s pattern matching offers more advanced constructs to refine and condense our code further.
Nested Patterns
Patterns can be nested to match inner values of complex data types.
enum Color {
RGB(u8, u8, u8),
HSV(u32, u8, u8),
}
let color = Color::RGB(255, 0, 0);
match color {
Color::RGB(255, green, blue) if green == 0 && blue == 0 => println!("This is red!"),
Color::RGB(..) => println!("Some RGB color"),
_ => println!("Some other color"),
}
At Bindings
The @
operator allows you to test a value against a pattern and bind the value to a variable.
let some_value = 5;
match some_value {
x @ 1..=5 => println!("Value is in the range 1-5 and is: {}", x),
_ => println!("Value is out of range"),
}
Multiple Patterns
Using the operator, you can match against various patterns, allowing for cleaner code in cases where multiple patterns lead to the same result.
let character = 'a';
match character {
'a' | 'e' | 'i' | 'o' | 'u' => println!("Vowel"),
_ => println!("Consonant or not a letter"),
}
Match Guards
These were briefly introduced before, but it’s worth noting that match guards (conditions after the if
keyword) offer additional flexibility when patterns alone aren't enough.
let pair = (5, -5);
match pair {
(x, y) if x + y == 0 => println!("Sum is zero"),
_ => println!("Sum isn't zero"),
}
Refutable and Irrefutable Patterns
Patterns come in two flavours:
- Irrefutable Patterns: These are patterns that will always match. For example, when you use
let x = 5;
,x
is an irrefutable pattern. - Refutable Patterns: These can fail to match. For example,
Some(x)
is refutable because a value can also beNone
.
Understanding the distinction is crucial when working with constructs like if let
and while let
which are designed to work with refutable patterns, providing a shorter syntax for matching single variants.
if let Some(value) = Some("Rustacean") {
println!("Hello, {}!", value);
}
Patterns Everywhere
Pattern matching in Rust isn’t confined to just the match
construct. The language has been designed to make patterns ubiquitous, ensuring that the same principles can be applied in various scenarios.
if let and while let
Apart from match
, the if let
and while let
constructs are Rust's way of bringing pattern matching to regular control flow. They're instrumental when you're only interested in one variant.
let optional = Some(7);
if let Some(i) = optional {
println!("Value is: {}", i);
}
In the case of while let
, the loop will continue to run as long as the pattern matches:
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
println!("{}", top);
}
For Loops
Pattern matching can be seamlessly integrated into for loops as well:
let points = [(1, 2), (3, 4), (5, 6)];
for (x, y) in points.iter() {
println!("x: {}, y: {}", x, y);
}
Function Parameters
Function or closure parameters can also benefit from pattern matching:
fn show_coords(&(x, y): &(i32, i32)) {
println!("x: {}, y: {}", x, y);
}
let point = (3, 4);
show_coords(&point);
Match Ergonomics
In the Rust 2018 edition, the language introduced improvements to pattern-matching ergonomics. One of the standout features is the ability to reference the inner values of an Option<&T>
a Result<&T, E>
. This removes some verbosity and makes the patterns feel more natural.
let x: Option<&i32> = Some(&5);
if let Some(y) = x {
println!("{}", y);
}
Note that the print statement does not need to dereference, thanks to match ergonomics.
Beyond Basic Patterns: Advanced Techniques
While we’ve already delved deep into pattern matching, Rust offers even more advanced tools and techniques to help developers fine-tune their code.
Binding Modes
Introduced with match ergonomics, binding modes help Rust determine whether it needs to dereference a value. When you match against a reference, Rust will automatically dereference it for you:
let x = Some(10);
if let Some(y) = &x {
println!("Matched {}", y);
}
Here, even though &x
is a reference, Rust allows you to write the pattern as if it wasn't, making the code cleaner.
@ Patterns for Advanced Binding
The @
symbol can be used to capture values in a pattern while also testing them against a sub-pattern:
let some_value = 5;
match some_value {
e @ 1..=5 => println!("Value is in the range 1-5 and is: {}", e),
_ => println!("Value is out of range"),
}
This powerful tool allows you to match and bind a value simultaneously.
Nested OR Patterns
Starting in Rust 1.53.0, nested OR patterns allow you to match against multiple patterns within a nested pattern. This can further condense match arms.
enum Message {
Hello { id: i32 },
Goodbye { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: 3 | 5 | 7 } => {
println!("Hello to a special ID!");
}
_ => {}
}
.. in Patterns
The ..
syntax can be used to ignore the rest of an item's value. This is especially useful when dealing with large structs or enums where you only care about one or two fields.
struct Point3D {
x: i32,
y: i32,
z: i32,
}
let point = Point3D { x: 1, y: 2, z: 3 };
match point {
Point3D { x, .. } => println!("Only care about x which is: {}", x),
}
The combination of concise destructuring, exhaustiveness checks, and its ubiquitous presence makes pattern matching a star feature of Rust, setting a high bar for other languages.
Whether you’re extracting values, handling errors, or navigating complex data structures, pattern matching remains an indispensable tool in every Rustacean’s toolbox.
Check out more articles about Rust in my Rust Programming Library!
Stay tuned, and happy coding!
Visit my Blog for more articles, news, and software engineering stuff!
Follow me on Medium, LinkedIn, and Twitter.
All the best,
CTO | Tech Lead | Senior Software Engineer | Cloud Solutions Architect | Rust 🦀 | Golang | Java | ML AI & Statistics | Web3 & Blockchain