Rust - Match Expressions

Overview

Estimated time: 55–75 minutes

Master Rust's powerful match expressions - one of Rust's most distinctive features. Learn pattern matching, destructuring, guards, and how match ensures exhaustive handling of all possible cases.

Learning Objectives

Prerequisites

Basic Match Syntax

Simple Match Expression

fn main() {
    let number = 3;
    
    let description = match number {
        1 => "one",
        2 => "two", 
        3 => "three",
        4 => "four",
        5 => "five",
        _ => "something else", // Catch-all pattern
    };
    
    println!("The number {} is {}", number, description);
    
    // Match can be used in different contexts
    match number {
        1..=5 => println!("Small number"),
        6..=10 => println!("Medium number"),
        _ => println!("Large number"),
    }
}

Expected output:

The number 3 is three
Small number

Pattern Matching with Enums

Basic Enum Matching

#[derive(Debug)]
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

fn move_player(direction: Direction) {
    match direction {
        Direction::Up => println!("Moving up"),
        Direction::Down => println!("Moving down"),
        Direction::Left => println!("Moving left"),  
        Direction::Right => println!("Moving right"),
    }
}

fn main() {
    let dir = Direction::Up;
    move_player(dir);
    
    let directions = vec![Direction::Left, Direction::Right, Direction::Down];
    for d in directions {
        move_player(d);
    }
}

Expected output:

Moving up
Moving left
Moving right
Moving down

Matching Option and Result

fn safe_divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 {
        None
    } else {
        Some(a / b)
    }
}

fn main() {
    let results = vec![
        safe_divide(10.0, 2.0),
        safe_divide(8.0, 0.0),
        safe_divide(15.0, 3.0),
    ];
    
    for result in results {
        match result {
            Some(value) => println!("Result: {:.2}", value),
            None => println!("Cannot divide by zero!"),
        }
    }
    
    // Parsing strings
    let inputs = vec!["42", "abc", "123"];
    
    for input in inputs {
        let parse_result: Result<i32, _> = input.parse();
        match parse_result {
            Ok(number) => println!("Parsed '{}': {}", input, number),
            Err(_) => println!("Failed to parse '{}'", input),
        }
    }
}

Expected output:

Result: 5.00
Cannot divide by zero!
Result: 5.00
Parsed '42': 42
Failed to parse 'abc'
Parsed '123': 123

Destructuring in Match

Struct Destructuring

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn describe_point(point: Point) {
    match point {
        Point { x: 0, y: 0 } => println!("Origin point"),
        Point { x: 0, y } => println!("On Y-axis at y = {}", y),
        Point { x, y: 0 } => println!("On X-axis at x = {}", x),
        Point { x, y } if x == y => println!("Diagonal point at ({}, {})", x, y),
        Point { x, y } => println!("Regular point at ({}, {})", x, y),
    }
}

fn main() {
    let points = vec![
        Point { x: 0, y: 0 },
        Point { x: 0, y: 5 },
        Point { x: 3, y: 0 },
        Point { x: 4, y: 4 },
        Point { x: 2, y: 7 },
    ];
    
    for point in points {
        describe_point(point);
    }
}

Expected output:

Origin point
On Y-axis at y = 5
On X-axis at x = 3
Diagonal point at (4, 4)
Regular point at (2, 7)

Match Guards and Advanced Patterns

Using Match Guards

fn categorize_number(n: i32) {
    match n {
        x if x < 0 => println!("{} is negative", x),
        0 => println!("Zero"),
        x if x % 2 == 0 => println!("{} is positive and even", x),
        x if x % 2 == 1 => println!("{} is positive and odd", x),
        _ => unreachable!(), // This should never be reached
    }
}

fn main() {
    let numbers = vec![-5, 0, 2, 7, 10];
    
    for num in numbers {
        categorize_number(num);
    }
}

Expected output:

-5 is negative
Zero
2 is positive and even
7 is positive and odd
10 is positive and even

Practical Example: Simple Calculator

Expression Evaluator

#[derive(Debug, Clone)]
enum Expression {
    Number(f64),
    Add(Box<Expression>, Box<Expression>),
    Subtract(Box<Expression>, Box<Expression>),
    Multiply(Box<Expression>, Box<Expression>),
    Divide(Box<Expression>, Box<Expression>),
}

fn evaluate(expr: &Expression) -> Result<f64, String> {
    match expr {
        Expression::Number(n) => Ok(*n),
        Expression::Add(left, right) => {
            let l = evaluate(left)?;
            let r = evaluate(right)?;
            Ok(l + r)
        },
        Expression::Subtract(left, right) => {
            let l = evaluate(left)?;
            let r = evaluate(right)?;
            Ok(l - r)
        },
        Expression::Multiply(left, right) => {
            let l = evaluate(left)?;
            let r = evaluate(right)?;
            Ok(l * r)
        },
        Expression::Divide(left, right) => {
            let l = evaluate(left)?;
            let r = evaluate(right)?;
            if r == 0.0 {
                Err("Division by zero".to_string())
            } else {
                Ok(l / r)
            }
        },
    }
}

fn main() {
    // Create expression: (10 + 5) * 2 - 8 / 4
    let expr = Expression::Subtract(
        Box::new(Expression::Multiply(
            Box::new(Expression::Add(
                Box::new(Expression::Number(10.0)),
                Box::new(Expression::Number(5.0))
            )),
            Box::new(Expression::Number(2.0))
        )),
        Box::new(Expression::Divide(
            Box::new(Expression::Number(8.0)),
            Box::new(Expression::Number(4.0))
        ))
    );
    
    match evaluate(&expr) {
        Ok(result) => println!("Result: {}", result),
        Err(error) => println!("Error: {}", error),
    }
}

Expected output:

Result: 28

Match vs If Let

When to Use Each Approach

fn main() {
    let maybe_number = Some(42);
    
    // Use match for exhaustive handling
    let doubled = match maybe_number {
        Some(n) => n * 2,
        None => 0,
    };
    println!("Doubled: {}", doubled);
    
    // Use if let for single pattern matching
    if let Some(n) = maybe_number {
        println!("The number is: {}", n);
    } else {
        println!("No number found");
    }
    
    // Match is better for multiple related patterns
    let status_code = 404;
    let message = match status_code {
        200 => "OK",
        404 => "Not Found", 
        500 => "Internal Server Error",
        _ => "Unknown Status",
    };
    println!("Status: {}", message);
}

Expected output:

Doubled: 84
The number is: 42
Status: Not Found

Checks for Understanding

Question 1: Exhaustiveness

Why does Rust require match expressions to be exhaustive?

Show Answer

Answer: Exhaustive matching ensures that all possible cases are handled, preventing runtime errors from unhandled cases. This is part of Rust's safety guarantees - the compiler ensures you've considered all possibilities, making your code more reliable and preventing common bugs.

Question 2: Match Guards

What's the purpose of match guards and when would you use them?

Show Answer

Answer: Match guards (the if condition after a pattern) allow you to add additional conditions to pattern matching. They're useful when you need to match on a pattern but also check additional conditions, like value ranges or comparisons with other variables.

Question 3: Pattern Binding

In the pattern Some(x) if x > 5, what happens to the variable x?

Show Answer

Answer: The variable x is bound to the value inside the Some variant and can be used both in the guard condition and in the match arm body. If the guard condition fails, the pattern doesn't match and x is not accessible.


← PreviousNext →