2024-10-12

008 - Pattern Matching in Rust for Python Developers

Learning Rust as a Pythonista: Pattern Matching

In this article, we’ll explore Pattern Matching in Rust, one of the language’s most powerful and flexible features. Pattern matching allows you to match complex data structures and handle different cases with safety and conciseness. Python introduced Structural Pattern Matching in Python 3.10 (PEP 634), but Rust’s pattern matching is more deeply integrated into the language and is an essential part of its design.

1. Pattern Matching in Python

With the release of Python 3.10, structural pattern matching was introduced, allowing Python to handle more complex cases using the match statement. Here’s an example of Python’s pattern matching:

Python Example (Pattern Matching):

def process(value):
    match value:
        case 0:
            return "Zero"
        case 1:
            return "One"
        case _:
            return "Other"

print(process(0))  # Output: Zero
print(process(2))  # Output: Other

In this example, Python’s match statement evaluates the value and matches it against different cases.

Key Points in Python:

  • New Feature: Structural pattern matching was introduced in Python 3.10, and it allows for more concise handling of various data patterns.
  • Basic Use: Python’s match works well with basic types like integers, tuples, and custom objects but is still evolving in flexibility and complexity.

2. Pattern Matching in Rust

Rust’s pattern matching is a core feature of the language, and it goes beyond simple matching of values. Rust allows you to match against structs, enums, tuples, and more, while enforcing exhaustive matches at compile time, ensuring all cases are handled.

Rust match Example:

fn process(value: i32) -> &'static str {
    match value {
        0 => "Zero",
        1 => "One",
        _ => "Other",
    }
}

fn main() {
    println!("{}", process(0));  // Output: Zero
    println!("{}", process(2));  // Output: Other
}

Just like in Python, the match statement in Rust matches the value against different patterns. The _ pattern is a catch-all, ensuring that any unmatched values are handled.

Key Differences:

  • Exhaustiveness: Rust enforces that all possible cases are handled, either by explicitly listing them or using the wildcard pattern (_). This ensures that no cases are missed, enhancing code safety.
  • Compile-time Safety: Rust’s pattern matching is checked at compile time, preventing runtime errors caused by missing cases.

3. Pattern Matching with Enums

One of the most powerful uses of pattern matching in Rust is with enums. Unlike Python’s enums, Rust enums can hold data, making pattern matching a crucial tool for handling the different variants.

Rust Enum and Pattern Matching Example:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn process_message(msg: Message) {
    match msg {
        Message::Quit => println!("Quit"),
        Message::Move { x, y } => println!("Move to {}, {}", x, y),
        Message::Write(text) => println!("Text message: {}", text),
        Message::ChangeColor(r, g, b) => println!("Change color to {}, {}, {}", r, g, b),
    }
}

fn main() {
    let msg = Message::Move { x: 10, y: 20 };
    process_message(msg);
}

Here, the match statement destructures the enum variants, allowing you to access the data inside each variant and handle them accordingly.

Key Differences:

  • Data in Enums: Unlike Python’s enums, Rust enums can store data in each variant, which allows for more expressive pattern matching.
  • Pattern Destructuring: Rust’s pattern matching allows you to destructure complex data types like enums, structs, and tuples directly in the match arm.

4. Matching on Structs and Tuples

Rust’s pattern matching can also be used to destructure structs and tuples, allowing you to easily access their fields and handle various cases based on their content.

Struct Pattern Matching Example:

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

fn print_point(point: Point) {
    match point {
        Point { x: 0, y } => println!("On the y-axis at {}", y),
        Point { x, y: 0 } => println!("On the x-axis at {}", x),
        Point { x, y } => println!("Point at ({}, {})", x, y),
    }
}

fn main() {
    let p = Point { x: 0, y: 5 };
    print_point(p);
}

In this example, Rust destructures the Point struct within the match arms to access its fields, allowing different behavior based on the values of x and y.

Key Differences:

  • Destructuring: Rust allows you to destructure structs, tuples, and other data types directly in pattern matching, making it easy to handle complex data structures.
  • Type Safety: Rust ensures that the types being matched are correct at compile time, catching potential errors early.

Tuple Pattern Matching Example:

fn print_coordinates(coords: (i32, i32)) {
    match coords {
        (0, y) => println!("On the y-axis at {}", y),
        (x, 0) => println!("On the x-axis at {}", x),
        (x, y) => println!("Coordinates: ({}, {})", x, y),
    }
}

fn main() {
    let coords = (0, 5);
    print_coordinates(coords);
}

This example demonstrates how Rust can match against tuples, extracting individual values and processing them accordingly.

5. Advanced Patterns in Rust

Rust’s pattern matching also supports more advanced patterns, including guards, binding variables in patterns, and ignoring parts of a pattern.

Pattern Guards

Pattern guards allow you to add conditions to patterns, giving you more control over how patterns are matched.

fn process_value(x: i32) {
    match x {
        n if n < 0 => println!("Negative number"),
        n if n == 0 => println!("Zero"),
        n if n > 0 => println!("Positive number"),
        _ => println!("No match"),
    }
}

fn main() {
    process_value(-5);  // Output: Negative number
    process_value(0);   // Output: Zero
    process_value(10);  // Output: Positive number
}

In this example, the if conditions in the match arms are used to control how the values are matched.

Binding in Patterns

Rust allows you to bind parts of a pattern to variables so you can reference them in the match arm.

fn process_option(opt: Option<i32>) {
    match opt {
        Some(x) if x > 10 => println!("Got a big number: {}", x),
        Some(x) => println!("Got a number: {}", x),
        None => println!("Got nothing"),
    }
}

fn main() {
    process_option(Some(20));  // Output: Got a big number: 20
    process_option(Some(5));   // Output: Got a number: 5
    process_option(None);      // Output: Got nothing
}

In this case, Some(x) binds the value inside the Option to x, which can then be used within the arm.

6. Refutability and Exhaustiveness

Rust’s pattern matching enforces exhaustiveness, meaning that all possible cases must be handled in a match expression. This is checked at compile time, ensuring that there are no unhandled cases, which can lead to safer code.

Example of Exhaustive Matching:

fn process_number(n: i32) {
    match n {
        0 => println!("Zero"),
        1 => println!("One"),
        _ => println!("Other"),
    }
}

If you omit the _ arm in this example, Rust will raise a compile-time error because not all cases are covered. This is different from Python, where unmatched cases will result in a runtime error.

Conclusion

Pattern matching in Rust is one of the language’s most powerful features, allowing you to concisely and safely destructure complex data structures and handle multiple cases. While Python introduced structural pattern matching in Python 3.10, Rust’s system is more deeply integrated and versatile, offering advanced features like destructuring, pattern guards, and exhaustive checking.

In the next article, we’ll explore Rust’s Memory Model and compare it with Python’s garbage collection system, focusing on Ownership and Borrowing. Stay tuned!

Running the Complete Rust Example

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

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

fn process_message(msg: Message) {
    match msg {
        Message::Quit => println!("Quit"),
        Message::Move { x, y } => println!("Move to {}, {}", x, y),
        Message::Write(text) => println!("Text message: {}", text),
        Message::ChangeColor(r, g, b) => println!("Change color to {}, {}, {}", r, g, b),
    }
}

fn print_point(point: Point) {
    match point {
        Point { x: 0, y } => println!("On the y-axis at {}", y),
        Point { x, y: 0 } => println!("On the x-axis at {}", x),
        Point { x, y } => println!("Point at ({}, {})", x, y),
    }
}

fn process_value(x: i32) {
    match x {
        n if n < 0 => println!("Negative number"),
        n if n == 0 => println!("Zero"),
        n if n > 0 => println!("Positive number"),
        _ => println!("No match"),
    }
}

fn print_coordinates(coords: (i32, i32)) {
    match coords {
        (0, y) => println!("On the y-axis at {}", y),
        (x, 0) => println!("On the x-axis at {}", x),
        (x, y) => println!("Coordinates: ({}, {})", x, y),
    }
}

fn process_option(opt: Option<i32>) {
    match opt {
        Some(x) if x > 10 => println!("Got a big number: {}", x),
        Some(x) => println!("Got a number: {}", x),
        None => println!("Got nothing"),
    }
}

fn main() {
    let msg = Message::Move { x: 10, y: 20 };
    process_message(msg);

    let p = Point { x: 0, y: 5 };
    print_point(p);

    process_value(-5);
    process_value(0);
    process_value(10);

    let coords = (0, 5);
    print_coordinates(coords);

    process_option(Some(20));
    process_option(Some(5));
    process_option(None);
}

Join the Journey Ahead!

If you're eager to continue this learning journey and stay updated with the latest insights, consider subscribing. By joining our mailing list, you'll receive notifications about new articles, tips, and resources to help you seamlessly pick up Rust by leveraging your Python skills.

Other articles in the series:

000 - Learning Rust as a Pythonista: A Suggested Path

001 - Learning Rust as a Pythonista: How to Create and Run a Rust File

002 - Learning Rust as a Pythonista: Basic Syntax and Structure

003 - Error Handling

004 - Structs and Enums

005 - Iterators and Closures

006 - Rust Traits vs. Python Duck Typing: A Comparison for Pythonistas

007 - Concurrency in Rust for Python Developers

009 - Macros in Rust for Python Developers