1. rust
  2. /structs enums
  3. /enums-pattern-matching

Enums & Pattern Matching

Enums (enumerations) allow you to define types that can be one of several variants. Combined with Rust's powerful pattern matching, enums enable expressive and safe code that handles different states and conditions elegantly.

Basic Enum Definition

Enums define a type with multiple possible variants:

enum Direction {
    North,
    South,
    East,
    West,
}

fn main() {
    let direction = Direction::North;
    
    match direction {
        Direction::North => println!("Going north!"),
        Direction::South => println!("Going south!"),
        Direction::East => println!("Going east!"),
        Direction::West => println!("Going west!"),
    }
}

Enums with Data

Enum variants can contain data:

enum Message {
    Quit,                       // No data
    Move { x: i32, y: i32 },   // Named fields (like a struct)
    Write(String),             // Single field
    ChangeColor(i32, i32, i32), // Multiple fields (like a tuple)
}

fn process_message(message: Message) {
    match message {
        Message::Quit => println!("Quitting..."),
        Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
        Message::Write(text) => println!("Writing: {}", text),
        Message::ChangeColor(r, g, b) => println!("Changing color to RGB({}, {}, {})", r, g, b),
    }
}

fn main() {
    let messages = vec![
        Message::Quit,
        Message::Move { x: 10, y: 20 },
        Message::Write(String::from("Hello, world!")),
        Message::ChangeColor(255, 0, 0),
    ];
    
    for message in messages {
        process_message(message);
    }
}

Pattern Matching with Match

The match expression is Rust's primary pattern matching construct:

Basic Match Patterns

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn describe_coin(coin: Coin) -> String {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            String::from("Penny")
        }
        Coin::Nickel => String::from("Nickel"),
        Coin::Dime => String::from("Dime"),
        Coin::Quarter => String::from("Quarter"),
    }
}

Matching with Guards

Add conditions to match arms with guards:

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

fn main() {
    let numbers = vec![-5, 0, 3, 15, 150];
    for num in numbers {
        println!("{} is {}", num, categorize_number(num));
    }
}

Matching Multiple Patterns

fn is_weekend(day: &str) -> bool {
    match day {
        "Saturday" | "Sunday" => true,
        _ => false,
    }
}

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

Advanced Enum Patterns

Recursive Enums

Enums can contain themselves (with indirection):

#[derive(Debug)]
enum List<T> {
    Cons(T, Box<List<T>>),
    Nil,
}

impl<T> List<T> {
    fn new() -> Self {
        List::Nil
    }
    
    fn prepend(self, elem: T) -> Self {
        List::Cons(elem, Box::new(self))
    }
    
    fn len(&self) -> usize {
        match self {
            List::Cons(_, tail) => 1 + tail.len(),
            List::Nil => 0,
        }
    }
}

fn main() {
    let list = List::new()
        .prepend(1)
        .prepend(2)
        .prepend(3);
    
    println!("List: {:?}", list);
    println!("Length: {}", list.len());
}

Generic Enums

#[derive(Debug)]
enum Result<T, E> {
    Ok(T),
    Err(E),
}

#[derive(Debug)]
enum Option<T> {
    Some(T),
    None,
}

// Custom generic enum
#[derive(Debug)]
enum Either<L, R> {
    Left(L),
    Right(R),
}

impl<L, R> Either<L, R> {
    fn is_left(&self) -> bool {
        matches!(self, Either::Left(_))
    }
    
    fn is_right(&self) -> bool {
        matches!(self, Either::Right(_))
    }
}

fn main() {
    let left: Either<i32, String> = Either::Left(42);
    let right: Either<i32, String> = Either::Right("hello".to_string());
    
    println!("Left is left: {}", left.is_left());
    println!("Right is right: {}", right.is_right());
}

Working with Option<T>

Option<T> represents a value that might be present or absent:

Basic Option Usage

fn find_word(text: &str, word: &str) -> Option<usize> {
    text.find(word)
}

fn main() {
    let text = "Hello, world!";
    
    match find_word(text, "world") {
        Some(index) => println!("Found 'world' at index: {}", index),
        None => println!("'world' not found"),
    }
    
    // Using if let for simpler cases
    if let Some(index) = find_word(text, "Hello") {
        println!("Found 'Hello' at index: {}", index);
    }
}

Option Methods

fn option_methods_example() {
    let some_number = Some(5);
    let no_number: Option<i32> = None;
    
    // unwrap_or: provide default value
    println!("Value or default: {}", some_number.unwrap_or(0));
    println!("Value or default: {}", no_number.unwrap_or(0));
    
    // map: transform the contained value
    let doubled = some_number.map(|x| x * 2);
    println!("Doubled: {:?}", doubled);
    
    // and_then: chain operations that return Option
    let result = some_number.and_then(|x| {
        if x > 0 {
            Some(x * 2)
        } else {
            None
        }
    });
    println!("Chained result: {:?}", result);
    
    // filter: keep value only if predicate is true
    let filtered = some_number.filter(|&x| x > 3);
    println!("Filtered: {:?}", filtered);
}

Option Pattern Matching

fn process_optional_data(data: Option<&str>) {
    match data {
        Some("") => println!("Got empty string"),
        Some(text) if text.len() > 10 => println!("Got long text: {}", text),
        Some(text) => println!("Got text: {}", text),
        None => println!("No data provided"),
    }
}

fn main() {
    process_optional_data(Some("Hello"));
    process_optional_data(Some(""));
    process_optional_data(Some("This is a very long string"));
    process_optional_data(None);
}

Working with Result<T, E>

Result<T, E> represents operations that can succeed or fail:

Basic Result Usage

use std::num::ParseIntError;

fn parse_number(s: &str) -> Result<i32, ParseIntError> {
    s.parse()
}

fn divide(a: f64, b: f64) -> Result<f64, &'static str> {
    if b == 0.0 {
        Err("Division by zero")
    } else {
        Ok(a / b)
    }
}

fn main() {
    // Pattern matching with Result
    match parse_number("42") {
        Ok(num) => println!("Parsed number: {}", num),
        Err(e) => println!("Parse error: {}", e),
    }
    
    // Using if let for success case only
    if let Ok(result) = divide(10.0, 2.0) {
        println!("Division result: {}", result);
    }
    
    // Handling error case
    match divide(10.0, 0.0) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),
    }
}

Result Methods

fn result_methods_example() -> Result<i32, &'static str> {
    let good_result: Result<i32, &str> = Ok(10);
    let bad_result: Result<i32, &str> = Err("something went wrong");
    
    // unwrap_or: provide default value for error case
    println!("Good result or default: {}", good_result.unwrap_or(0));
    println!("Bad result or default: {}", bad_result.unwrap_or(0));
    
    // map: transform success value
    let doubled = good_result.map(|x| x * 2);
    println!("Doubled: {:?}", doubled);
    
    // map_err: transform error value
    let mapped_error = bad_result.map_err(|e| format!("Error: {}", e));
    println!("Mapped error: {:?}", mapped_error);
    
    // and_then: chain operations that return Result
    let chained = good_result.and_then(|x| {
        if x > 5 {
            Ok(x * 2)
        } else {
            Err("too small")
        }
    });
    println!("Chained: {:?}", chained);
    
    Ok(42)
}

The ? Operator

The ? operator provides a concise way to propagate errors:

use std::fs::File;
use std::io::{self, Read};

fn read_file_content(filename: &str) -> Result<String, io::Error> {
    let mut file = File::open(filename)?; // Propagate error if file doesn't open
    let mut content = String::new();
    file.read_to_string(&mut content)?;   // Propagate error if read fails
    Ok(content)
}

fn calculate_sum(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
    let num_a = a.parse::<i32>()?;
    let num_b = b.parse::<i32>()?;
    Ok(num_a + num_b)
}

fn main() {
    match calculate_sum("10", "20") {
        Ok(sum) => println!("Sum: {}", sum),
        Err(e) => println!("Parse error: {}", e),
    }
    
    match calculate_sum("10", "abc") {
        Ok(sum) => println!("Sum: {}", sum),
        Err(e) => println!("Parse error: {}", e),
    }
}

Advanced Pattern Matching

Destructuring Complex Patterns

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

enum Shape {
    Circle { center: Point, radius: f64 },
    Rectangle { top_left: Point, bottom_right: Point },
    Triangle(Point, Point, Point),
}

fn describe_shape(shape: Shape) {
    match shape {
        Shape::Circle { center: Point { x, y }, radius } => {
            println!("Circle at ({}, {}) with radius {}", x, y, radius);
        }
        Shape::Rectangle { 
            top_left: Point { x: x1, y: y1 }, 
            bottom_right: Point { x: x2, y: y2 } 
        } => {
            println!("Rectangle from ({}, {}) to ({}, {})", x1, y1, x2, y2);
        }
        Shape::Triangle(p1, p2, p3) => {
            println!("Triangle with points ({}, {}), ({}, {}), ({}, {})", 
                     p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
        }
    }
}

Nested Pattern Matching

enum Message {
    Text(String),
    Number(i32),
    Nested(Box<Message>),
    Batch(Vec<Message>),
}

fn process_message(message: Message) {
    match message {
        Message::Text(text) if text.len() > 10 => {
            println!("Long text: {}", text);
        }
        Message::Text(text) => {
            println!("Short text: {}", text);
        }
        Message::Number(n) if n < 0 => {
            println!("Negative number: {}", n);
        }
        Message::Number(n) => {
            println!("Positive number: {}", n);
        }
        Message::Nested(boxed_msg) => {
            println!("Processing nested message:");
            process_message(*boxed_msg);
        }
        Message::Batch(messages) => {
            println!("Processing batch of {} messages:", messages.len());
            for msg in messages {
                process_message(msg);
            }
        }
    }
}

Pattern Matching with References

fn match_references() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    for number in &numbers {
        match number {
            1 => println!("One!"),
            2 | 3 => println!("Two or three"),
            4..=10 => println!("Between four and ten"),
            _ => println!("Something else"),
        }
    }
    
    // Pattern matching with dereferencing
    let option_number = Some(42);
    match &option_number {
        Some(&n) if n > 40 => println!("Big number: {}", n),
        Some(&n) => println!("Small number: {}", n),
        None => println!("No number"),
    }
}

Useful Pattern Matching Techniques

Using matches! Macro

enum Status {
    Active,
    Inactive,
    Pending,
}

fn main() {
    let status = Status::Active;
    
    // Instead of match
    let is_active = match status {
        Status::Active => true,
        _ => false,
    };
    
    // Use matches! macro
    let is_active = matches!(status, Status::Active);
    println!("Is active: {}", is_active);
    
    // More complex patterns
    let numbers = vec![1, 2, 3, 4, 5];
    let count = numbers.iter().filter(|&&x| matches!(x, 1 | 2 | 3)).count();
    println!("Count of 1, 2, or 3: {}", count);
}

Ignoring Values with _

enum Color {
    Rgb(u8, u8, u8),
    Hsv(u16, u8, u8),
}

fn process_color(color: Color) {
    match color {
        Color::Rgb(r, _, _) => println!("Red component: {}", r),
        Color::Hsv(h, _, _) => println!("Hue: {}", h),
    }
}

fn ignore_patterns() {
    let point = (3, 5, 7);
    
    match point {
        (x, ..) => println!("x coordinate: {}", x), // Ignore remaining fields
    }
    
    let numbers = (1, 2, 3, 4, 5);
    match numbers {
        (first, .., last) => println!("First: {}, Last: {}", first, last),
    }
}

Variable Binding with @

fn binding_examples() {
    let number = 42;
    
    match number {
        n @ 1..=50 => println!("Small number: {}", n),
        n @ 51..=100 => println!("Medium number: {}", n),
        n => println!("Large number: {}", n),
    }
    
    enum Message {
        Hello { id: i32 },
    }
    
    let msg = Message::Hello { id: 5 };
    
    match msg {
        Message::Hello { id: id_var @ 3..=7 } => {
            println!("Found id in range: {}", id_var);
        }
        Message::Hello { id } => {
            println!("Found other id: {}", id);
        }
    }
}

Best Practices

1. Use Exhaustive Pattern Matching

enum State {
    Loading,
    Success(String),
    Error(String),
}

// Good: Handle all cases
fn handle_state(state: State) {
    match state {
        State::Loading => println!("Loading..."),
        State::Success(data) => println!("Success: {}", data),
        State::Error(err) => println!("Error: {}", err),
    }
}

// Avoid: Using _ when you should handle specific cases
fn handle_state_bad(state: State) {
    match state {
        State::Success(data) => println!("Success: {}", data),
        _ => println!("Not success"), // Loses information
    }
}

2. Use if let for Simple Cases

// Good for simple Option/Result handling
fn simple_option_handling(opt: Option<i32>) {
    if let Some(value) = opt {
        println!("Got value: {}", value);
    }
}

// Use match for complex cases
fn complex_option_handling(opt: Option<i32>) {
    match opt {
        Some(n) if n > 0 => println!("Positive: {}", n),
        Some(n) if n < 0 => println!("Negative: {}", n),
        Some(0) => println!("Zero"),
        None => println!("No value"),
    }
}

3. Prefer Descriptive Enum Names

// Good: Clear intent
enum PaymentStatus {
    Pending,
    Processing,
    Completed,
    Failed { reason: String },
}

// Avoid: Unclear meaning
enum Status {
    A,
    B,
    C,
    D(String),
}

4. Use Methods on Enums

enum ConnectionState {
    Disconnected,
    Connecting,
    Connected { address: String },
    Error { message: String },
}

impl ConnectionState {
    fn is_connected(&self) -> bool {
        matches!(self, ConnectionState::Connected { .. })
    }
    
    fn address(&self) -> Option<&str> {
        match self {
            ConnectionState::Connected { address } => Some(address),
            _ => None,
        }
    }
}

Enums and pattern matching are fundamental to idiomatic Rust programming. They enable safe handling of different states and conditions while maintaining code clarity and preventing common bugs. Master these concepts to write more robust and expressive Rust code.