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…

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

  1. Irrefutable Patterns: These are patterns that will always match. For example, when you use let x = 5;, x is an irrefutable pattern.
  2. Refutable Patterns: These can fail to match. For example, Some(x) is refutable because a value can also be None.

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

Read more