Pattern matching in Rust

Greetings fellow Rustaceans 🦀!

In this short post, I’ll try to present a series of succint yet hopefully comprehensive examples of pattern matching in Rust.

Rust has many cool features and pattern matching is one of them.

I would like to keep this short, so let’s dive right into it.

Example 1: Basics of the match keyword

Our exploration of pattern matching starts with the match keyword. It is a versatile conductor orchestrating decisions based on data. Whether dealing with numbers, strings, or custom structs, the match construct ensures a fluid and stylistically aligned code execution.

Here is a very basic and contrived example, to start with:

fn main() {
    let n = 7;

    match n {
        1 => println!("Too low."), // not matched
        7 => println!("Lucky number 7!"), // **matched!**
        42 => println!("The answer to life, the universe, and everything..."), // not matched
        _ => println!("A numerical insignificance."), // not matched
    }
}
// prints: Lucky number 7!

Example 2: Structs

Here is how we can do pattern matching on structs:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let my_point = Point { x: 42, y: 24 };

    match my_point {
        Point { x, y } => {
            println!("X-coordinate: {}, Y-coordinate: {}", x, y);
        }
    }
}
// prints: X-coordinate: 42, Y-coordinate: 24

Example 3: Enums

In pattern matching, enums shine by providing a structured way to handle the different cases. The example showcases the handling of different variants of the Message enum:

enum Message {
    Greeting(String),
    Farewell,
    Custom(String, i32),
}

fn main() {
    let my_message = Message::Custom(String::from("Salutations"), 42);

    match my_message {
        Message::Greeting(msg) => {
            println!("Received a salutation: {}", msg);
        }
        Message::Farewell => {
            println!("Bidding adieu!");
        }
        Message::Custom(text, number) => {
            println!("Custom message: {} - {}", text, number);
        }
    }
}
// prints: Custom message: Salutations - 42

Example 4: The @ operator

The @ opeartor creates a binding to a variable.

fn main() {
    let point = (3, 7);

    match point {
        (x_within_range @ 1..=5, y_within_range @ 1..=10) => {
            println!(
                "Point within specified bounds: ({}, {})",
                x_within_range, y_within_range
            );
        }
        (x, y) => {
            println!("Point outside specified bounds: ({}, {})", x, y);
        }
    }
}
// prints: Point within specified bounds: (3, 7)

Example 5: More of the @ operator

Here is another example where, when matched, we bind the name “Alice” to the variable special_name:

fn main() {
    let name = "Alice";

    match name {
        special_name @ "Alice" => {
            println!(
                "Special greeting for Alice: Hello, {}!",
                special_name.to_uppercase()
            );
        }
        x => {
            println!("Generic greeting: Hi, {}!", x);
        }
    }
}
// prints: Special greeting for Alice: Hello, ALICE!

Example 6: Mixing things

We can mix match with | and if in order to create much more expressive and flexible matches, where we chain patterns together , as shown here:

fn main() {
    let my_favorite_fruit = ("banana", 3);

    match my_favorite_fruit {
        ("banana", quantity) | ("apple", quantity) if quantity > 2 => {
            println!("An abundance of fruit!");
        }
        ("orange", _) => {
            println!("Evoking citrusy vibes!");
        }
        _ => {
            println!("Merely a snack.");
        }
    }
}
// prints: An abundance of fruit!

Example 7: Ranges and guards

We already saw guards and conditional expressions within patterns, but here is a bit more involved example to illustrate the point:

fn main() {
    let number = 42;

    match number {
        n if n < 0 => println!("Manifesting negative connotations!"),
        n if (0..=10).contains(&n) => println!("Diminutive yet potent!"),
        n if n > 10 && n % 2 == 0 => println!("Balancing the odds!"),
        _ => println!("A numerical insignificance."),
    }
}
// prints: Balncing the odds!

Example 8: The @ operator once again

Here we revisit the @ to ephasize its utility in capturing specific values within patterns providing a refined and precise approach to matching:

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

fn main() {
    let my_person = Person {
        name: String::from("Alice"),
        age: 30,
    };

    match my_person {
        Person {
            name,
            age: age_within_range @ 20..=35,
        } => {
            println!(
                "{} falls within the age range of 20-35 ({} years old).",
                name, age_within_range
            );
        }
        Person { name, age } => {
            println!(
                "{} does not fall within the specified age range ({} years old).",
                name, age
            );
        }
    }
}
// prints: Alice falls within the age range of 20-35 (30 years old).

Hopefully someone will find this post useful, but at the very least I know that future Self (see what I did there?) will use it as a reference. Happy coding!