Rust - Advanced Patterns

Overview

Estimated time: 40–50 minutes

Master advanced pattern matching techniques in Rust, including match guards, range patterns, destructuring complex data structures, @ bindings, and pattern refutability. Build on basic pattern matching to handle sophisticated matching scenarios.

Learning Objectives

Prerequisites

Match Guards

Conditional Pattern Matching

Match guards allow you to add conditions to patterns using the if keyword:

fn classify_number(x: i32) -> &'static str {
    match x {
        n if n < 0 => "negative",
        n if n == 0 => "zero",
        n if n > 0 && n <= 10 => "small positive",
        n if n > 10 && n <= 100 => "medium positive",
        _ => "large positive",
    }
}

fn main() {
    println!("{}", classify_number(-5));  // negative
    println!("{}", classify_number(0));   // zero
    println!("{}", classify_number(7));   // small positive
    println!("{}", classify_number(50));  // medium positive
    println!("{}", classify_number(200)); // large positive
}

Guards with Destructuring

Combine guards with pattern destructuring:

struct Person {
    name: String,
    age: u32,
    city: String,
}

fn categorize_person(person: &Person) -> &'static str {
    match person {
        Person { age, city, .. } if *age < 18 => "minor",
        Person { age, city, .. } if *age >= 18 && city == "New York" => "adult New Yorker",
        Person { age, city, .. } if *age >= 65 => "senior citizen",
        Person { city, .. } if city == "San Francisco" => "San Francisco resident",
        _ => "regular adult",
    }
}

fn main() {
    let person1 = Person {
        name: String::from("Alice"),
        age: 16,
        city: String::from("Boston"),
    };
    
    let person2 = Person {
        name: String::from("Bob"),
        age: 30,
        city: String::from("New York"),
    };
    
    println!("{}", categorize_person(&person1)); // minor
    println!("{}", categorize_person(&person2)); // adult New Yorker
}

Range Patterns

Numeric Ranges

Use range patterns to match values within a range:

fn classify_grade(score: u32) -> &'static str {
    match score {
        90..=100 => "A",
        80..=89 => "B",
        70..=79 => "C",
        60..=69 => "D",
        0..=59 => "F",
        _ => "Invalid score",
    }
}

fn main() {
    println!("{}", classify_grade(95)); // A
    println!("{}", classify_grade(73)); // C
    println!("{}", classify_grade(45)); // F
}

Character Ranges

Range patterns work with characters too:

fn classify_char(c: char) -> &'static str {
    match c {
        'a'..='z' => "lowercase letter",
        'A'..='Z' => "uppercase letter",
        '0'..='9' => "digit",
        ' ' | '\t' | '\n' => "whitespace",
        _ => "other character",
    }
}

fn main() {
    println!("{}", classify_char('m')); // lowercase letter
    println!("{}", classify_char('5')); // digit
    println!("{}", classify_char('@')); // other character
}

Complex Destructuring

Nested Structure Destructuring

Destructure deeply nested structures:

struct Address {
    street: String,
    city: String,
    country: String,
}

struct Company {
    name: String,
    address: Address,
}

struct Employee {
    name: String,
    company: Company,
    salary: Option,
}

fn describe_employee(employee: &Employee) -> String {
    match employee {
        Employee {
            name,
            company: Company {
                name: company_name,
                address: Address { city, country, .. }
            },
            salary: Some(sal),
        } if *sal > 100000 => {
            format!("{} is a high earner at {} in {}, {}", name, company_name, city, country)
        }
        Employee {
            name,
            company: Company { name: company_name, .. },
            salary: Some(sal),
        } => {
            format!("{} works at {} earning ${}", name, company_name, sal)
        }
        Employee {
            name,
            company: Company { name: company_name, .. },
            salary: None,
        } => {
            format!("{} works at {} (salary unknown)", name, company_name)
        }
    }
}

fn main() {
    let employee = Employee {
        name: String::from("Alice"),
        company: Company {
            name: String::from("TechCorp"),
            address: Address {
                street: String::from("123 Tech St"),
                city: String::from("San Francisco"),
                country: String::from("USA"),
            },
        },
        salary: Some(120000),
    };
    
    println!("{}", describe_employee(&employee));
    // Alice is a high earner at TechCorp in San Francisco, USA
}

@ Bindings

Binding While Matching

Use @ to bind values while pattern matching:

enum Message {
    Hello { id: i32, name: String },
    Quit,
    Move { x: i32, y: i32 },
}

fn process_message(msg: Message) {
    match msg {
        Message::Hello { id: id_var @ 1..=5, name } => {
            println!("Hello {} with low ID: {}", name, id_var);
        }
        Message::Hello { id: id_var @ 6..=10, name } => {
            println!("Hello {} with medium ID: {}", name, id_var);
        }
        Message::Hello { id, name } => {
            println!("Hello {} with ID: {}", name, id);
        }
        Message::Move { x: x_pos @ 0..=10, y: y_pos @ 0..=10 } => {
            println!("Moving to small coordinates: ({}, {})", x_pos, y_pos);
        }
        Message::Move { x, y } => {
            println!("Moving to: ({}, {})", x, y);
        }
        Message::Quit => println!("Quit message received"),
    }
}

fn main() {
    process_message(Message::Hello { id: 3, name: String::from("Alice") });
    // Hello Alice with low ID: 3
    
    process_message(Message::Move { x: 5, y: 8 });
    // Moving to small coordinates: (5, 8)
}

Multiple Patterns

OR Patterns

Match multiple patterns using the | operator:

fn day_type(day: &str) -> &'static str {
    match day {
        "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" => "weekday",
        "Saturday" | "Sunday" => "weekend",
        _ => "unknown day",
    }
}

fn is_vowel(c: char) -> bool {
    match c.to_ascii_lowercase() {
        'a' | 'e' | 'i' | 'o' | 'u' => true,
        _ => false,
    }
}

fn main() {
    println!("{}", day_type("Wednesday")); // weekday
    println!("{}", day_type("Saturday"));  // weekend
    println!("{}", is_vowel('A'));         // true
    println!("{}", is_vowel('B'));         // false
}

Pattern Matching in Different Contexts

if let with Guards

Use complex patterns in if let expressions:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    if let Some(first @ 1..=3) = numbers.first() {
        println!("First number is small: {}", first);
    }
    
    let point = (3, 4);
    if let (x, y) if x * x + y * y == 25 = point {
        println!("Point ({}, {}) is on circle with radius 5", x, y);
    }
}

while let with Patterns

Use patterns in while let loops:

fn main() {
    let mut stack = vec![1, 2, 3, 4, 5];
    
    while let Some(top @ 3..=5) = stack.pop() {
        println!("Popped large number: {}", top);
    }
    
    println!("Remaining: {:?}", stack); // [1, 2]
}

Refutable vs Irrefutable Patterns

Understanding Pattern Types

Patterns can be refutable (can fail to match) or irrefutable (always match):

fn main() {
    // Irrefutable patterns - always match
    let x = 5;                    // Variable binding
    let (a, b) = (1, 2);         // Tuple destructuring
    let Point { x, y } = point;  // Struct destructuring (if point is Point)
    
    // Refutable patterns - may not match
    if let Some(value) = some_option {  // Option may be None
        println!("Got value: {}", value);
    }
    
    match some_result {
        Ok(value) => println!("Success: {}", value),
        Err(e) => println!("Error: {}", e),
    }
}

Advanced Destructuring Examples

Array and Slice Patterns

Destructure arrays and slices with patterns:

fn analyze_slice(slice: &[i32]) {
    match slice {
        [] => println!("Empty slice"),
        [x] => println!("Single element: {}", x),
        [x, y] => println!("Two elements: {}, {}", x, y),
        [first, .., last] => println!("First: {}, Last: {}", first, last),
        [first, second, rest @ ..] if rest.len() > 2 => {
            println!("First: {}, Second: {}, {} more elements", first, second, rest.len());
        }
        _ => println!("Other pattern"),
    }
}

fn main() {
    analyze_slice(&[]);           // Empty slice
    analyze_slice(&[1]);          // Single element: 1
    analyze_slice(&[1, 2]);       // Two elements: 1, 2
    analyze_slice(&[1, 2, 3]);    // First: 1, Last: 3
    analyze_slice(&[1, 2, 3, 4, 5, 6]); // First: 1, Second: 2, 4 more elements
}

Common Pitfalls

Mistakes to Avoid

Checks for Understanding

  1. What's the difference between x @ 1..=10 and just 1..=10?
  2. When should you use match guards instead of separate if statements?
  3. What makes a pattern refutable vs irrefutable?
  4. How do you match the first and last elements of a slice while ignoring the middle?
Answers
  1. x @ 1..=10 binds the matched value to x so you can use it; 1..=10 just matches without binding
  2. Use match guards when the condition depends on the matched values or when you want to keep related patterns together
  3. Refutable patterns can fail (like Some(x)); irrefutable patterns always succeed (like variable bindings)
  4. Use [first, .., last] to match first and last elements while ignoring middle elements

← PreviousNext →