On this page

Enums and pattern matching

15 min read TextCh. 3 — Compound Types

Enums: representing multiple shapes

Enums (enumerations) in Rust are far more powerful than in most languages. Each variant can carry different data, making Rust's enums equivalent to "sum types" or "union types" from type theory.

Basic enums

#[derive(Debug)]
enum DayOfWeek {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday,
}

fn is_workday(day: &DayOfWeek) -> bool {
    matches!(day, DayOfWeek::Monday | DayOfWeek::Tuesday | DayOfWeek::Wednesday
             | DayOfWeek::Thursday | DayOfWeek::Friday)
}

fn main() {
    let today = DayOfWeek::Wednesday;
    println!("Today: {:?}", today);
    println!("Is workday? {}", is_workday(&today));
}

Enums with data

The most powerful feature: each variant can carry different types of data:

#[derive(Debug)]
enum Message {
    Quit,                                          // No data
    Move { x: i32, y: i32 },                       // Anonymous struct
    Write(String),                                  // A String
    Color(u8, u8, u8),                             // Three u8 (RGB)
    Attachment { name: String, bytes: Vec<u8> },   // Struct with Vec
}

impl Message {
    fn process(&self) {
        match self {
            Message::Quit => println!("Shutting down..."),
            Message::Move { x, y } => println!("Moving to ({x}, {y})"),
            Message::Write(t) => println!("Text received: {t}"),
            Message::Color(r, g, b) => println!("Color: rgb({r}, {g}, {b})"),
            Message::Attachment { name, bytes } => {
                println!("Attachment '{}' ({} bytes)", name, bytes.len())
            }
        }
    }
}

fn main() {
    let messages = vec![
        Message::Move { x: 10, y: 20 },
        Message::Write(String::from("Hello!")),
        Message::Color(255, 128, 0),
        Message::Attachment {
            name: String::from("photo.jpg"),
            bytes: vec![0xFF, 0xD8, 0xFF],
        },
        Message::Quit,
    ];
    
    for m in &messages {
        m.process();
    }
}

Option: goodbye to null

Rust has no null. Instead, it uses the Option<T> enum from the standard library:

enum Option<T> {
    Some(T),  // There is a value
    None,     // There is no value
}
fn find_index(list: &[i32], target: i32) -> Option<usize> {
    for (i, &n) in list.iter().enumerate() {
        if n == target {
            return Some(i);
        }
    }
    None
}

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

fn main() {
    let numbers = [3, 1, 4, 1, 5, 9, 2, 6];
    
    match find_index(&numbers, 5) {
        Some(i) => println!("Found at index {i}"),
        None    => println!("Not found"),
    }
    
    // Convenient Option methods
    let result = divide(10.0, 3.0);
    println!("Result: {:.4}", result.unwrap_or(0.0));
    
    let nothing = divide(5.0, 0.0);
    println!("Division by zero: {}", nothing.is_none());
    
    // map: transform the value if it exists
    let doubled = result.map(|r| r * 2.0);
    println!("Doubled: {:.4}", doubled.unwrap_or(0.0));
    
    // and_then: chain operations that can fail
    let text = Some("42");
    let number: Option<i32> = text.and_then(|s| s.parse().ok());
    println!("Number: {:?}", number);
}

Result: error handling

Result<T, E> is the other fundamental enum from the standard library:

enum Result<T, E> {
    Ok(T),   // Successful operation with value T
    Err(E),  // Error of type E
}
use std::num::ParseIntError;

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

fn main() {
    let cases = ["42", " -7 ", "hello", "999999999999"];
    
    for case in &cases {
        match parse_number(case) {
            Ok(n)  => println!("'{case}' → {n}"),
            Err(e) => println!("'{case}' → Error: {e}"),
        }
    }
}

match: the heart of pattern matching

match is Rust's most powerful operator for working with enums. It is exhaustive: it must cover all possible cases:

fn describe_number(n: i32) -> &'static str {
    match n {
        i32::MIN..=-1 => "negative",
        0             => "zero",
        1..=9         => "digit",
        10..=99       => "two digits",
        100..=999     => "three digits",
        _             => "large",
    }
}

fn classify_char(c: char) -> &'static str {
    match c {
        'a'..='z' | 'á' | 'é' | 'í' | 'ó' | 'ú' => "lowercase letter",
        'A'..='Z' | 'Á' | 'É' | 'Í' | 'Ó' | 'Ú' => "uppercase letter",
        '0'..='9' => "digit",
        ' ' | '\t' | '\n' => "whitespace",
        _ => "symbol",
    }
}

fn main() {
    for n in [-5, 0, 7, 42, 500, 10_000] {
        println!("{n}: {}", describe_number(n));
    }
    
    for c in ['a', 'Z', '5', ' ', '@'] {
        println!("'{c}': {}", classify_char(c));
    }
}

Guards in match

Guards allow adding extra conditions to match arms:

fn evaluate_temperature(temp: f64) -> &'static str {
    match temp {
        t if t < -30.0 => "dangerously cold",
        t if t < 0.0   => "below freezing",
        t if t < 15.0  => "cold",
        t if t < 25.0  => "comfortable",
        t if t < 35.0  => "warm",
        t if t < 40.0  => "very hot",
        _              => "dangerously hot",
    }
}

fn main() {
    for temp in [-40.0, -5.0, 10.0, 22.0, 33.0, 38.0, 42.0] {
        println!("{temp:.1}°C: {}", evaluate_temperature(temp));
    }
}

if let and while let: concise pattern matching

When you only care about one case of the enum:

fn main() {
    let config: Option<u32> = Some(8080);
    
    // With match (verbose for a single case):
    match config {
        Some(port) => println!("Configured port: {port}"),
        None => (), // Do nothing
    }
    
    // With if let (more concise):
    if let Some(port) = config {
        println!("Port: {port}");
    }
    
    // if let with else:
    if let Some(port) = config {
        println!("Custom port: {port}");
    } else {
        println!("Using default port: 3000");
    }
    
    // while let: iterate until the pattern no longer matches
    let mut stack = vec![1, 2, 3, 4, 5];
    while let Some(top) = stack.pop() {
        print!("{top} ");
    }
    println!();
    
    // Chaining if let
    let value: Result<Option<i32>, String> = Ok(Some(42));
    if let Ok(Some(n)) = value {
        println!("Nested value: {n}");
    }
}

Nested patterns and destructuring

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

fn main() {
    let point = Point { x: 3, y: -5 };
    
    // Destructuring in match
    let description = match point {
        Point { x: 0, y: 0 } => "origin",
        Point { x, y: 0 }    => "on the X axis",
        Point { x: 0, y }    => "on the Y axis",
        Point { x, y } if x == y => "on the diagonal",
        _                    => "arbitrary point",
    };
    println!("{description}");
    
    // Binding with @
    let n = 15;
    match n {
        x @ 1..=10  => println!("{x} is between 1 and 10"),
        x @ 11..=20 => println!("{x} is between 11 and 20"),
        x           => println!("{x} is out of range"),
    }
    
    // Tuples in match
    let coordinates = (1, -1);
    match coordinates {
        (0, 0) => println!("Origin"),
        (x, 0) | (0, x) => println!("On axis with {x}"),
        (x, y) if x == -y => println!("Anti-diagonal"),
        (x, y) => println!("({x}, {y})"),
    }
}

Enums and pattern matching make Rust code extraordinarily expressive. The next lesson covers collections from the standard library — Vec, String, and HashMap.

match is exhaustive
The compiler verifies that all possible cases of an enum are covered in a match. If you add a new variant to an enum, the compiler will warn you in every place that has a match not handling it. This prevents 'forgotten case' bugs.
Option<T> replaces null
Rust has no null. Instead it uses Option<T> with variants Some(T) for a present value and None for absence. This forces the programmer to handle the 'no value' case explicitly, eliminating NullPointerExceptions from the Rust world.
rust
#[derive(Debug)]
enum Shape {
    Circle(f64),
    Rectangle { width: f64, height: f64 },
    Triangle(f64, f64, f64),
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(r) => std::f64::consts::PI * r * r,
            Shape::Rectangle { width, height } => width * height,
            Shape::Triangle(a, b, c) => {
                // Heron's formula
                let s = (a + b + c) / 2.0;
                (s * (s - a) * (s - b) * (s - c)).sqrt()
            }
        }
    }

    fn name(&self) -> &str {
        match self {
            Shape::Circle(_)         => "Circle",
            Shape::Rectangle { .. }  => "Rectangle",
            Shape::Triangle(..)      => "Triangle",
        }
    }
}

fn main() {
    let shapes: Vec<Shape> = vec![
        Shape::Circle(5.0),
        Shape::Rectangle { width: 4.0, height: 6.0 },
        Shape::Triangle(3.0, 4.0, 5.0),
    ];

    for shape in &shapes {
        println!("{}: area = {:.4}", shape.name(), shape.area());
    }
}