As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!
Pattern matching in Rust represents one of the language's most powerful features. Going beyond simple conditional logic, it provides a declarative approach to controlling program flow based on the structure and content of data. I've found pattern matching particularly valuable when working with complex data structures and state transitions in my projects.
At its core, Rust's pattern matching revolves around the match
expression. Unlike switch statements in other languages, match
in Rust is exhaustive, meaning the compiler verifies that all possible values are handled. This prevents an entire class of runtime errors by catching missing cases during compilation.
fn describe_number(x: i32) {
match x {
0 => println!("Zero"),
1 => println!("One"),
2 => println!("Two"),
_ => println!("Many"),
}
}
The underscore pattern (_
) serves as a catchall, handling any values not explicitly matched. When working with enums, pattern matching truly shines:
enum WebEvent {
PageLoad,
KeyPress(char),
Click { x: i64, y: i64 }
}
fn handle_event(event: WebEvent) {
match event {
WebEvent::PageLoad => println!("Page loaded"),
WebEvent::KeyPress(c) => println!("Pressed key: {}", c),
WebEvent::Click { x, y } => println!("Clicked at x={}, y={}", x, y),
}
}
Destructuring patterns allow extracting values from complex data structures. This approach works with tuples, structs, and enums, making code more concise and readable:
struct Point {
x: i32,
y: i32,
}
let point = Point { x: 10, y: 20 };
match point {
Point { x, y } => println!("Point at ({}, {})", x, y),
}
// You can also destructure with specific values
match point {
Point { x: 0, y } => println!("On y-axis at {}", y),
Point { x, y: 0 } => println!("On x-axis at {}", x),
Point { x, y } => println!("Point at ({}, {})", x, y),
}
When I need to match against a specific pattern while retaining the original value, the @
binding operator becomes invaluable:
fn inspect_integer(x: i32) {
match x {
n @ 0..=10 => println!("{} is between 0 and 10", n),
n @ 11..=20 => println!("{} is between 11 and 20", n),
n => println!("{} is greater than 20", n),
}
}
For cases where I'm only interested in one specific pattern, the if let
construct provides a concise alternative to a full match:
let optional = Some(7);
// Instead of:
match optional {
Some(value) => println!("Got a value: {}", value),
None => (),
}
// You can write:
if let Some(value) = optional {
println!("Got a value: {}", value);
}
Similarly, while let
allows continuing a loop as long as a pattern matches:
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
println!("Stack top: {}", top);
}
The real power of pattern matching comes with match guards, which add conditional logic to pattern matches:
fn describe_number_and_condition(x: i32) {
match x {
n if n % 2 == 0 => println!("{} is even", n),
n if n % 2 == 1 => println!("{} is odd", n),
_ => unreachable!(), // All integers are either even or odd
}
}
Pattern matching also works well with references and mutable references:
fn inspect_reference(value: &i32) {
match value {
&0 => println!("Zero by reference"),
&n => println!("Non-zero by reference: {}", n),
}
}
// Or more concisely with ref patterns
fn inspect_value(value: i32) {
match value {
ref r if *r > 10 => println!("Reference to value > 10: {}", r),
_ => println!("Other value"),
}
}
When working with complex enums containing multiple variants, pattern matching makes handling all cases straightforward:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn process_message(msg: Message) {
match msg {
Message::Quit => println!("Quitting"),
Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
Message::Write(text) => println!("Text message: {}", text),
Message::ChangeColor(r, g, b) => println!("Changing color to ({}, {}, {})", r, g, b),
}
}
Nested structures can also be destructured in a single pattern match:
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Shape {
Circle(f64, Color),
Rectangle(f64, f64, Color),
}
fn describe_shape(shape: Shape) {
match shape {
Shape::Circle(radius, Color::Rgb(r, g, b)) => {
println!("RGB colored circle with radius {}, color ({}, {}, {})", radius, r, g, b);
}
Shape::Circle(radius, Color::Hsv(h, s, v)) => {
println!("HSV colored circle with radius {}, color ({}, {}, {})", radius, h, s, v);
}
Shape::Rectangle(width, height, Color::Rgb(r, g, b)) => {
println!("RGB colored rectangle {}×{}, color ({}, {}, {})", width, height, r, g, b);
}
Shape::Rectangle(width, height, Color::Hsv(h, s, v)) => {
println!("HSV colored rectangle {}×{}, color ({}, {}, {})", width, height, h, s, v);
}
}
}
Multiple patterns can be matched with the |
operator, providing a concise way to handle several cases with the same code:
fn classify_char(c: char) {
match c {
'a' | 'e' | 'i' | 'o' | 'u' => println!("{} is a vowel", c),
'0'..='9' => println!("{} is a digit", c),
c if c.is_alphabetic() => println!("{} is a consonant", c),
_ => println!("{} is something else", c),
}
}
Range patterns are particularly useful when dealing with numeric or character ranges:
fn grade_score(score: u8) {
match score {
90..=100 => println!("A"),
80..=89 => println!("B"),
70..=79 => println!("C"),
60..=69 => println!("D"),
_ => println!("F"),
}
}
Slice patterns allow matching on array or slice contents:
fn analyze_slice(slice: &[i32]) {
match slice {
[] => println!("Empty slice"),
[single] => println!("Single element: {}", single),
[first, second] => println!("Two elements: {} and {}", first, second),
[first, rest @ ..] => println!("First element is {}, rest has {} elements", first, rest.len()),
}
}
When working with Option
and Result
types, pattern matching provides a safe and expressive way to handle different cases:
fn process_option(opt: Option<i32>) {
match opt {
Some(value) => println!("Got a value: {}", value),
None => println!("No value"),
}
}
fn process_result(res: Result<i32, String>) {
match res {
Ok(value) => println!("Success: {}", value),
Err(e) => println!("Error: {}", e),
}
}
Combining pattern matching with custom types creates powerful state machines:
enum State {
Start,
Processing { progress: f64 },
Success(String),
Failed(String),
}
fn handle_state_transition(current: State, event: &str) -> State {
match (current, event) {
(State::Start, "begin") => State::Processing { progress: 0.0 },
(State::Processing { progress }, "update") if progress < 0.9 => {
State::Processing { progress: progress + 0.1 }
},
(State::Processing { .. }, "finish") => State::Success("Operation completed".to_string()),
(State::Processing { .. }, "error") => State::Failed("Operation failed".to_string()),
(State::Success(_) | State::Failed(_), "restart") => State::Start,
(state, _) => state, // Default case: remain in current state
}
}
Exhaustiveness checking is a key safety feature of Rust's pattern matching. The compiler ensures you handle all possible cases, preventing bugs when types evolve. For example, if we add a variant to an enum, the compiler will flag all match expressions that need updating:
enum Payment {
Cash(f64),
Card(String, u16),
}
fn process_payment(payment: Payment) {
match payment {
Payment::Cash(amount) => println!("Paid ${:.2} in cash", amount),
Payment::Card(number, cvv) => println!("Paid with card ending in {}", &number[number.len()-4..]),
}
}
// If we later add:
// enum Payment {
// Cash(f64),
// Card(String, u16),
// Digital(String),
// }
// The compiler will flag the process_payment function as non-exhaustive
In practice, I've found pattern matching particularly useful for parsing and processing structured data:
enum JsonValue {
Null,
Boolean(bool),
Number(f64),
String(String),
Array(Vec<JsonValue>),
Object(HashMap<String, JsonValue>),
}
fn format_json_value(indent: usize, value: &JsonValue) -> String {
match value {
JsonValue::Null => "null".to_string(),
JsonValue::Boolean(b) => b.to_string(),
JsonValue::Number(n) => n.to_string(),
JsonValue::String(s) => format!("\"{}\"", s),
JsonValue::Array(items) => {
let formatted_items: Vec<String> = items
.iter()
.map(|item| format!("{}{}", " ".repeat(indent + 2), format_json_value(indent + 2, item)))
.collect();
format!("[\n{}\n{}]", formatted_items.join(",\n"), " ".repeat(indent))
}
JsonValue::Object(map) => {
let formatted_entries: Vec<String> = map
.iter()
.map(|(key, value)| {
format!("{}\"{}\": {}",
" ".repeat(indent + 2),
key,
format_json_value(indent + 2, value))
})
.collect();
format!("{{\n{}\n{}}}", formatted_entries.join(",\n"), " ".repeat(indent))
}
}
}
Pattern matching can also significantly improve error handling, making code both safer and more readable:
fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
match denominator {
0.0 => Err("Division by zero".to_string()),
d => Ok(numerator / d),
}
}
fn calculate_and_print(a: f64, b: f64) {
match divide(a, b) {
Ok(result) => println!("{} / {} = {}", a, b, result),
Err(e) => println!("Error: {}", e),
}
}
When working with multiple errors, an enum combined with pattern matching creates a cohesive error handling system:
enum MathError {
DivisionByZero,
NegativeSquareRoot,
Overflow,
}
fn sqrt(x: f64) -> Result<f64, MathError> {
if x < 0.0 {
Err(MathError::NegativeSquareRoot)
} else {
Ok(x.sqrt())
}
}
fn divide(a: f64, b: f64) -> Result<f64, MathError> {
if b == 0.0 {
Err(MathError::DivisionByZero)
} else {
Ok(a / b)
}
}
fn process_calculation(x: f64, y: f64) {
match divide(x, y).and_then(|result| sqrt(result)) {
Ok(value) => println!("Result: {}", value),
Err(MathError::DivisionByZero) => println!("Cannot divide by zero"),
Err(MathError::NegativeSquareRoot) => println!("Cannot take square root of negative number"),
Err(MathError::Overflow) => println!("Calculation resulted in overflow"),
}
}
Pattern matching becomes especially powerful when combined with Rust's move semantics and ownership rules:
enum Message {
Text(String),
Command(String, Vec<String>),
}
fn process_message(msg: Message) {
match msg {
Message::Text(content) => {
// content is moved here
println!("Text message: {}", content);
// content is valid until the end of this scope
}
Message::Command(name, args) => {
// name and args are moved here
println!("Command '{}' with {} arguments: {:?}", name, args.len(), args);
}
}
// msg is no longer valid here since it was moved into the match arms
}
For more sophisticated matching needs, patterns can be combined with custom destructors and functions:
struct User {
username: String,
role: String,
active: bool,
}
fn is_admin(user: &User) -> bool {
user.role == "admin" && user.active
}
fn handle_request(user: &User, action: &str) {
match (user, action) {
(user, "view_dashboard") => println!("Showing dashboard to {}", user.username),
(user, "admin_panel") if is_admin(user) => println!("Showing admin panel to {}", user.username),
(user, "admin_panel") => println!("Access denied for {} to admin panel", user.username),
(_, action) => println!("Unknown action: {}", action),
}
}
Pattern matching also integrates well with iterators and functional programming constructs:
fn process_numbers(numbers: Vec<i32>) {
let categorized: HashMap<&str, Vec<i32>> = numbers
.into_iter()
.fold(HashMap::new(), |mut acc, num| {
match num {
n if n < 0 => acc.entry("negative").or_insert_with(Vec::new).push(n),
0 => acc.entry("zero").or_insert_with(Vec::new).push(n),
n if n % 2 == 0 => acc.entry("even").or_insert_with(Vec::new).push(n),
n => acc.entry("odd").or_insert_with(Vec::new).push(n),
}
acc
});
for (category, nums) in categorized {
println!("{}: {:?}", category, nums);
}
}
In conclusion, Rust's pattern matching system stands as one of its most distinctive and powerful features. By combining expressive syntax with compile-time safety guarantees, pattern matching makes code more readable, maintainable, and correct. From simple enum handling to complex data extraction and state transitions, pattern matching provides a declarative approach to control flow that reduces bugs and improves code clarity. Whether you're handling errors, processing complex data structures, or implementing state machines, pattern matching should be a central part of your Rust programming toolkit.
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva
Top comments (0)