Guide contents

Lesson 21 of 94

Enums: Types With Variants

A Rust enum is a fixed set of variants, each of which can carry its own data. That makes enums one of the most useful features in the language.

Concept Rust
Named constants enum Direction { North, South }
Variants carrying data enum Shape { Circle(f64), Square(f64) }
Branching on a variant match (exhaustive, checked at compile time)
A value that may be absent Option<T>, itself an enum: next lesson
A value that may be an error Result<T, E>, also an enum: Result

1. A simple enum

Variants use PascalCase, and you refer to one with EnumName::Variant.

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

fn describe(d: Direction) -> &'static str {
    match d {
        Direction::North => "up",
        Direction::East => "right",
        Direction::South => "down",
        Direction::West => "left",
    }
}

fn main() {
    println!("{}", describe(Direction::North));
    println!("{}", describe(Direction::West));
}
Output
up
left

2. Deriving common behavior

Like structs, enums get debug printing, copying, and comparison through derive.

Rust
#[derive(Debug, Clone, Copy, PartialEq)]
enum Level {
    Debug,
    Info,
    Warn,
    Error,
}

fn main() {
    let configured = Level::Warn;
    let incoming = Level::Error;
    println!("{:?} vs {:?}", configured, incoming);
    println!("same level: {}", configured == incoming);
    let copy = configured; // Copy: `configured` stays usable
    println!("{:?} {:?}", copy, configured);
}
Output
Warn vs Error
same level: false
Warn Warn

3. match must handle every variant

This is the most important property of enums. Forget a variant and the code does not compile.

Rust✗ does not compile · E0004
enum PaymentStatus {
    Pending,
    Authorized,
    Captured,
    Refunded,
}

fn label(s: PaymentStatus) -> &'static str {
    match s {
        PaymentStatus::Pending => "pending",
        PaymentStatus::Authorized => "authorized",
        PaymentStatus::Captured => "captured",
    }
}

fn main() {
    println!("{}", label(PaymentStatus::Refunded));
}
Output
error[E0004]: non-exhaustive patterns: `PaymentStatus::Refunded` not covered
  --> src/main.rs:9:11
   |
 9 |     match s {
   |           ^ pattern `PaymentStatus::Refunded` not covered
   |
note: `PaymentStatus` defined here
  --> src/main.rs:1:6
   |
 1 | enum PaymentStatus {
   |      ^^^^^^^^^^^^^
...
 5 |     Refunded,
   |     -------- not covered
   = note: the matched value is of type `PaymentStatus`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
   |
12 ~         PaymentStatus::Captured => "captured",
13 ~         PaymentStatus::Refunded => todo!(),
   |

When a teammate adds a new variant, every match that needs updating becomes a compile error pointing at the exact line.

4. use to shorten variant names

use Enum::* brings the variants into scope so you can write them without the prefix. It is common inside a match on one enum.

Rust
#[derive(Debug, Clone, Copy)]
enum Suit {
    Hearts,
    Diamonds,
    Clubs,
    Spades,
}

fn color(s: Suit) -> &'static str {
    use Suit::*;
    match s {
        Hearts | Diamonds => "red",
        Clubs | Spades => "black",
    }
}

fn main() {
    for s in [Suit::Hearts, Suit::Clubs, Suit::Diamonds, Suit::Spades] {
        println!("{:?} is {}", s, color(s));
    }
}
Output
Hearts is red
Clubs is black
Diamonds is red
Spades is black

5. Explicit numeric values

A variant without data can have an explicit integer value, and as converts it to that number, and you control the numbers.

Rust
#[derive(Debug, Clone, Copy)]
enum HttpStatus {
    Ok = 200,
    NotFound = 404,
    InternalError = 500,
}

fn main() {
    let s = HttpStatus::NotFound;
    println!("{:?} = {}", s, s as u16);
    println!("{}", HttpStatus::Ok as i32 + 1);
}
Output
NotFound = 404
201

Converting a number back to an enum can fail (what is 418?), so Rust has no automatic reverse cast. You write it yourself, as in example 7.

6. Methods on enums

Enums get impl blocks exactly like structs.

Rust
#[derive(Debug, Clone, Copy, PartialEq)]
enum TrafficLight {
    Red,
    Green,
    Yellow,
}

impl TrafficLight {
    fn next(self) -> TrafficLight {
        match self {
            TrafficLight::Red => TrafficLight::Green,
            TrafficLight::Green => TrafficLight::Yellow,
            TrafficLight::Yellow => TrafficLight::Red,
        }
    }

    fn duration_secs(self) -> u32 {
        match self {
            TrafficLight::Red => 30,
            TrafficLight::Green => 25,
            TrafficLight::Yellow => 5,
        }
    }
}

fn main() {
    let mut light = TrafficLight::Red;
    for _ in 0..4 {
        println!("{:?} for {}s", light, light.duration_secs());
        light = light.next();
    }
}
Output
Red for 30s
Green for 25s
Yellow for 5s
Red for 30s

7. Converting from a number or string

An associated function builds a variant from outside data. Here unknown input maps to a catch-all variant; the next lesson shows the better tool, Option.

Rust
#[derive(Debug)]
enum Weekday {
    Mon, Tue, Wed, Thu, Fri, Sat, Sun,
    Invalid,
}

impl Weekday {
    fn from_number(n: u8) -> Self {
        match n {
            1 => Self::Mon,
            2 => Self::Tue,
            3 => Self::Wed,
            4 => Self::Thu,
            5 => Self::Fri,
            6 => Self::Sat,
            7 => Self::Sun,
            _ => Self::Invalid,
        }
    }

    fn is_weekend(&self) -> bool {
        matches!(self, Self::Sat | Self::Sun)
    }
}

fn main() {
    for n in [1, 6, 9] {
        let day = Weekday::from_number(n);
        println!("{n} -> {:?}, weekend: {}", day, day.is_weekend());
    }
}
Output
1 -> Mon, weekend: false
6 -> Sat, weekend: true
9 -> Invalid, weekend: false

matches!(value, pattern) returns true if the value matches the pattern. It is a compact way to ask "is it one of these variants?".

8. Variants that carry data

Each variant can hold values. A tuple-like variant lists its data in parentheses.

Rust
#[derive(Debug)]
enum Shape {
    Circle(f64),          // radius
    Rectangle(f64, f64),  // width, height
    Triangle(f64, f64, f64),
}

fn main() {
    let shapes = vec![
        Shape::Circle(1.5),
        Shape::Rectangle(3.0, 4.0),
        Shape::Triangle(3.0, 4.0, 5.0),
    ];
    for s in &shapes {
        println!("{:?}", s);
    }
}
Output
Circle(1.5)
Rectangle(3.0, 4.0)
Triangle(3.0, 4.0, 5.0)

A Vec<Shape> holds circles, rectangles, and triangles together.

9. Getting the data out with match

Patterns in a match arm name the data inside a variant, and those names are available in the arm.

Rust
enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
    Triangle(f64, f64, f64),
}

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

fn main() {
    let shapes = [Shape::Circle(1.0), Shape::Rectangle(2.0, 3.5), Shape::Triangle(3.0, 4.0, 5.0)];
    let mut total = 0.0;
    for s in &shapes {
        let a = s.area();
        total += a;
        println!("{a:.3}");
    }
    println!("total area {total:.3}");
}
Output
3.142
7.000
6.000
total area 16.142

10. Variants with named fields

A variant can also have named fields, like a struct.

Rust
#[derive(Debug)]
enum Event {
    Click { x: i32, y: i32 },
    KeyPress { key: char, shift: bool },
    Resize { width: u32, height: u32 },
    Quit,
}

fn handle(e: &Event) -> String {
    match e {
        Event::Click { x, y } => format!("click at ({x}, {y})"),
        Event::KeyPress { key, shift: true } => format!("key {}", key.to_ascii_uppercase()),
        Event::KeyPress { key, shift: false } => format!("key {key}"),
        Event::Resize { width, height } => format!("resize to {width}x{height}"),
        Event::Quit => String::from("quit"),
    }
}

fn main() {
    let events = [
        Event::Click { x: 10, y: 20 },
        Event::KeyPress { key: 'a', shift: true },
        Event::KeyPress { key: 'b', shift: false },
        Event::Resize { width: 800, height: 600 },
        Event::Quit,
    ];
    for e in &events {
        println!("{}", handle(e));
    }
}
Output
click at (10, 20)
key A
key b
resize to 800x600
quit

The pattern Event::KeyPress { key, shift: true } matches only key presses with shift held, and names the key field.

11. Mixing all kinds of variants

One enum can combine unit variants, tuple variants, and struct-like variants.

Rust
#[derive(Debug)]
enum Command {
    Ping,
    Echo(String),
    Move { x: i32, y: i32 },
    SetColor(u8, u8, u8),
}

fn main() {
    let commands = vec![
        Command::Ping,
        Command::Echo(String::from("hello")),
        Command::Move { x: 3, y: -1 },
        Command::SetColor(0, 128, 255),
    ];
    for c in &commands {
        match c {
            Command::Ping => println!("pong"),
            Command::Echo(text) => println!("{text}"),
            Command::Move { x, y } => println!("moving by ({x}, {y})"),
            Command::SetColor(r, g, b) => println!("color #{r:02x}{g:02x}{b:02x}"),
        }
    }
}
Output
pong
hello
moving by (3, -1)
color #0080ff

12. Matching through a reference binds references

When you match on a &Enum, the names in the patterns become references to the data, so nothing is moved out of the enum. This is why handle(e: &Event) above could read key and x without taking ownership.

Rust
enum Message {
    Text(String),
    Image { url: String, width: u32 },
}

fn preview(m: &Message) -> String {
    match m {
        Message::Text(body) => {
            // `body` is a &String here
            let short: String = body.chars().take(10).collect();
            format!("text: {short}...")
        }
        Message::Image { url, width } => format!("image {width}px from {url}"),
    }
}

fn main() {
    let inbox = vec![
        Message::Text(String::from("Meeting moved to 3pm tomorrow")),
        Message::Image { url: String::from("https://example.com/cat.png"), width: 640 },
    ];
    for m in &inbox {
        println!("{}", preview(m));
    }
    println!("inbox still has {} messages", inbox.len());
}
Output
text: Meeting mo...
image 640px from https://example.com/cat.png
inbox still has 2 messages

13. Changing the data inside a variant

Matching on &mut self gives &mut references to the fields.

Rust
#[derive(Debug)]
enum Account {
    Active { balance: i64 },
    Frozen { balance: i64, reason: String },
}

impl Account {
    fn apply_interest(&mut self, percent: i64) {
        match self {
            Account::Active { balance } => *balance += *balance * percent / 100,
            Account::Frozen { .. } => {} // frozen accounts earn nothing
        }
    }
}

fn main() {
    let mut accounts = vec![
        Account::Active { balance: 1_000 },
        Account::Frozen { balance: 5_000, reason: String::from("fraud review") },
    ];
    for a in &mut accounts {
        a.apply_interest(5);
    }
    println!("{:?}", accounts);
}
Output
[Active { balance: 1050 }, Frozen { balance: 5000, reason: "fraud review" }]

{ .. } ignores all the fields of a variant.

14. Consuming an enum to change state

A method taking self can return a new variant, moving data from the old state into the new one without cloning.

Rust
#[derive(Debug)]
enum Connection {
    Disconnected,
    Connecting { host: String, attempt: u32 },
    Connected { host: String, session_id: u64 },
}

impl Connection {
    fn step(self) -> Connection {
        match self {
            Connection::Disconnected => Connection::Connecting { host: String::from("db.internal"), attempt: 1 },
            Connection::Connecting { host, attempt } if attempt < 3 => {
                Connection::Connecting { host, attempt: attempt + 1 }
            }
            Connection::Connecting { host, .. } => Connection::Connected { host, session_id: 0xBEEF },
            connected => connected,
        }
    }
}

fn main() {
    let mut conn = Connection::Disconnected;
    for _ in 0..5 {
        println!("{:?}", conn);
        conn = conn.step();
    }
}
Output
Disconnected
Connecting { host: "db.internal", attempt: 1 }
Connecting { host: "db.internal", attempt: 2 }
Connecting { host: "db.internal", attempt: 3 }
Connected { host: "db.internal", session_id: 48879 }

if attempt < 3 is a match guard, from Control Flow. The last arm connected => connected matches anything else and returns it unchanged.

15. Enums holding structs

A common style is to define a struct for each variant's data, which lets you pass the data around on its own.

Rust
#[derive(Debug)]
struct CardPayment {
    last4: String,
    amount_cents: u64,
}

#[derive(Debug)]
struct BankTransfer {
    iban: String,
    amount_cents: u64,
}

#[derive(Debug)]
enum Payment {
    Card(CardPayment),
    Bank(BankTransfer),
    Cash(u64),
}

fn fee_cents(p: &Payment) -> u64 {
    match p {
        Payment::Card(c) => 30 + c.amount_cents * 29 / 1000,
        Payment::Bank(b) => if b.amount_cents > 100_000 { 0 } else { 50 },
        Payment::Cash(_) => 0,
    }
}

fn main() {
    let payments = [
        Payment::Card(CardPayment { last4: String::from("4242"), amount_cents: 10_000 }),
        Payment::Bank(BankTransfer { iban: String::from("DE89 3704"), amount_cents: 250_000 }),
        Payment::Cash(1_500),
    ];
    for p in &payments {
        println!("fee {:>3} for {:?}", fee_cents(p), p);
    }
}
Output
fee 320 for Card(CardPayment { last4: "4242", amount_cents: 10000 })
fee   0 for Bank(BankTransfer { iban: "DE89 3704", amount_cents: 250000 })
fee   0 for Cash(1500)

16. How big is an enum?

An enum is as large as its largest variant, plus a small tag saying which variant it is. There is no heap allocation unless a variant's data allocates.

Rust
enum Small {
    A,
    B,
    C,
}

enum Mixed {
    Flag(bool),
    Number(u64),
    Point(f64, f64),
}

fn main() {
    println!("Small: {} byte", std::mem::size_of::<Small>());
    println!("Mixed: {} bytes", std::mem::size_of::<Mixed>());
    println!("Option<Box<i32>>: {} bytes", std::mem::size_of::<Option<Box<i32>>>());
    let _ = (Small::A, Small::B, Small::C, Mixed::Flag(true), Mixed::Number(1), Mixed::Point(0.0, 0.0));
}
Output
Small: 1 byte
Mixed: 24 bytes
Option<Box<i32>>: 8 bytes

Mixed needs 16 bytes for its largest variant, Point(f64, f64), plus 8 bytes for the tag (rounded up for alignment). The last line shows a trick you will meet in Option: the compiler can sometimes hide the tag inside the data for free.

17. A recursive enum needs indirection

An enum that contains itself directly would be infinitely large, so the compiler rejects it:

Rust✗ does not compile · E0072
enum Expr {
    Number(f64),
    Add(Expr, Expr),
}

fn main() {}
Output
error[E0072]: recursive type `Expr` has infinite size
 --> src/main.rs:1:1
  |
1 | enum Expr {
  | ^^^^^^^^^
2 |     Number(f64),
3 |     Add(Expr, Expr),
  |         ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
3 |     Add(Box<Expr>, Expr),
  |         ++++    +

Box<T> stores a value on the heap and holds just a pointer to it, which has a fixed size. This fixes the problem. Box covers Box in detail.

18. An expression tree with Box

Rust
enum Expr {
    Number(f64),
    Add(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
    Neg(Box<Expr>),
}

impl Expr {
    fn eval(&self) -> f64 {
        match self {
            Expr::Number(n) => *n,
            Expr::Add(a, b) => a.eval() + b.eval(),
            Expr::Mul(a, b) => a.eval() * b.eval(),
            Expr::Neg(e) => -e.eval(),
        }
    }

    fn render(&self) -> String {
        match self {
            Expr::Number(n) => n.to_string(),
            Expr::Add(a, b) => format!("({} + {})", a.render(), b.render()),
            Expr::Mul(a, b) => format!("{} * {}", a.render(), b.render()),
            Expr::Neg(e) => format!("-{}", e.render()),
        }
    }
}

fn main() {
    // (2 + 3) * -4
    let e = Expr::Mul(
        Box::new(Expr::Add(Box::new(Expr::Number(2.0)), Box::new(Expr::Number(3.0)))),
        Box::new(Expr::Neg(Box::new(Expr::Number(4.0)))),
    );
    println!("{} = {}", e.render(), e.eval());
}
Output
(2 + 3) * -4 = -20

19. Vec already provides the indirection

A variant that holds a Vec of the same enum does not need Box, because a Vec stores its elements on the heap already. This is how a JSON value can be modeled.

Rust
enum Json {
    Null,
    Bool(bool),
    Number(f64),
    Str(String),
    Array(Vec<Json>),
    Object(Vec<(String, Json)>),
}

impl Json {
    fn render(&self) -> String {
        match self {
            Json::Null => String::from("null"),
            Json::Bool(b) => b.to_string(),
            Json::Number(n) => n.to_string(),
            Json::Str(s) => format!("\"{s}\""),
            Json::Array(items) => {
                let parts: Vec<String> = items.iter().map(|i| i.render()).collect();
                format!("[{}]", parts.join(","))
            }
            Json::Object(fields) => {
                let parts: Vec<String> = fields.iter().map(|(k, v)| format!("\"{k}\":{}", v.render())).collect();
                format!("{{{}}}", parts.join(","))
            }
        }
    }
}

fn main() {
    let doc = Json::Object(vec![
        (String::from("id"), Json::Number(7.0)),
        (String::from("name"), Json::Str(String::from("Ferris"))),
        (String::from("tags"), Json::Array(vec![Json::Str(String::from("rust")), Json::Bool(true)])),
        (String::from("manager"), Json::Null),
    ]);
    println!("{}", doc.render());
}
Output
{"id":7,"name":"Ferris","tags":["rust",true],"manager":null}

items.iter().map(|i| i.render()).collect() renders every element into a Vec<String>. It is an iterator pipeline, the Rust equivalent of items.stream().map(...).toList(), covered in Iterators.

20. if let for one variant

When you care about only one variant, if let is shorter than a full match. Pattern Matching covers it in depth.

Rust
enum Notification {
    Email { to: String, subject: String },
    Sms { number: String },
    Push,
}

fn main() {
    let queue = vec![
        Notification::Sms { number: String::from("+15550100") },
        Notification::Email { to: String::from("ops@example.com"), subject: String::from("Disk at 91%") },
        Notification::Push,
    ];
    for n in &queue {
        if let Notification::Email { to, subject } = n {
            println!("email to {to}: {subject}");
        }
    }
    let sms_count = queue.iter().filter(|n| matches!(n, Notification::Sms { .. })).count();
    println!("{sms_count} SMS in the queue");
}
Output
email to ops@example.com: Disk at 91%
1 SMS in the queue

21. Realistic example: parsing commands

Turning text into an enum at the edge of a program means the rest of the code works with well-typed values instead of strings.

Rust
#[derive(Debug)]
enum Command {
    Get(String),
    Set(String, String),
    Delete(String),
    List,
    Unknown(String),
}

fn parse(line: &str) -> Command {
    let parts: Vec<&str> = line.split_whitespace().collect();
    match parts.as_slice() {
        ["GET", key] => Command::Get(key.to_string()),
        ["SET", key, value] => Command::Set(key.to_string(), value.to_string()),
        ["DEL", key] => Command::Delete(key.to_string()),
        ["LIST"] => Command::List,
        _ => Command::Unknown(line.to_string()),
    }
}

fn main() {
    for line in ["SET color blue", "GET color", "DEL color", "LIST", "FLY away"] {
        println!("{:?}", parse(line));
    }
}
Output
Set("color", "blue")
Get("color")
Delete("color")
List
Unknown("FLY away")

parts.as_slice() gives a slice, and each arm matches its exact shape, like the slice patterns in Slices.

22. Realistic example: a key-value store driven by commands

Rust
enum Command {
    Set(String, String),
    Get(String),
    Delete(String),
}

struct Store {
    entries: Vec<(String, String)>,
}

impl Store {
    fn execute(&mut self, cmd: Command) -> String {
        match cmd {
            Command::Set(key, value) => {
                for entry in &mut self.entries {
                    if entry.0 == key {
                        entry.1 = value;
                        return String::from("OK (updated)");
                    }
                }
                self.entries.push((key, value));
                String::from("OK")
            }
            Command::Get(key) => {
                for (k, v) in &self.entries {
                    if *k == key {
                        return v.clone();
                    }
                }
                String::from("(nil)")
            }
            Command::Delete(key) => {
                let before = self.entries.len();
                self.entries.retain(|(k, _)| *k != key);
                format!("deleted {}", before - self.entries.len())
            }
        }
    }
}

fn main() {
    let mut store = Store { entries: Vec::new() };
    let script = vec![
        Command::Set(String::from("user:1"), String::from("ada")),
        Command::Get(String::from("user:1")),
        Command::Set(String::from("user:1"), String::from("grace")),
        Command::Get(String::from("user:1")),
        Command::Delete(String::from("user:1")),
        Command::Get(String::from("user:1")),
    ];
    for cmd in script {
        println!("{}", store.execute(cmd));
    }
}
Output
OK
ada
OK (updated)
grace
deleted 1
(nil)

execute takes each Command by value, so the Strings inside move straight into the store without cloning. retain keeps only elements for which the closure returns true; see Closures.

23. Realistic example: event sourcing

Instead of storing an order's current state, store the events that happened to it, and compute the state by replaying them.

Rust
#[derive(Debug)]
enum OrderEvent {
    Created { id: u32 },
    ItemAdded { sku: String, qty: u32, price_cents: u64 },
    ItemRemoved { sku: String },
    Paid { amount_cents: u64 },
    Shipped { carrier: String },
}

#[derive(Debug, Default)]
struct OrderState {
    id: u32,
    items: Vec<(String, u32, u64)>,
    paid_cents: u64,
    shipped_with: String,
}

fn replay(events: &[OrderEvent]) -> OrderState {
    let mut state = OrderState::default();
    for e in events {
        match e {
            OrderEvent::Created { id } => state.id = *id,
            OrderEvent::ItemAdded { sku, qty, price_cents } => state.items.push((sku.clone(), *qty, *price_cents)),
            OrderEvent::ItemRemoved { sku } => state.items.retain(|(s, _, _)| s != sku),
            OrderEvent::Paid { amount_cents } => state.paid_cents += amount_cents,
            OrderEvent::Shipped { carrier } => state.shipped_with = carrier.clone(),
        }
    }
    state
}

fn main() {
    let events = vec![
        OrderEvent::Created { id: 12 },
        OrderEvent::ItemAdded { sku: String::from("BOOK-1"), qty: 2, price_cents: 1_500 },
        OrderEvent::ItemAdded { sku: String::from("PEN-9"), qty: 1, price_cents: 300 },
        OrderEvent::ItemRemoved { sku: String::from("PEN-9") },
        OrderEvent::Paid { amount_cents: 3_000 },
        OrderEvent::Shipped { carrier: String::from("DHL") },
    ];
    println!("{:#?}", replay(&events));
    println!("state after 3 events: {:?}", replay(&events[..3]).items);
}
Output
OrderState {
    id: 12,
    items: [
        (
            "BOOK-1",
            2,
            1500,
        ),
    ],
    paid_cents: 3000,
    shipped_with: "DHL",
}
state after 3 events: [("BOOK-1", 2, 1500), ("PEN-9", 1, 300)]

Because replay takes a slice, replaying only the first three events is just &events[..3].

24. Realistic example: a vending machine state machine

Each state is a variant, and each input produces the next state. The compiler checks that every state handles every input.

Rust
#[derive(Debug, Clone, Copy, PartialEq)]
enum State {
    Idle,
    HasMoney(u32),
    Dispensing,
}

#[derive(Debug, Clone, Copy)]
enum Input {
    InsertCoin(u32),
    PressButton,
    Refund,
    Done,
}

const PRICE: u32 = 150;

fn transition(state: State, input: Input) -> (State, &'static str) {
    match (state, input) {
        (State::Idle, Input::InsertCoin(c)) => (State::HasMoney(c), "coin accepted"),
        (State::HasMoney(m), Input::InsertCoin(c)) => (State::HasMoney(m + c), "coin accepted"),
        (State::HasMoney(m), Input::PressButton) if m >= PRICE => (State::Dispensing, "dispensing"),
        (State::HasMoney(_), Input::PressButton) => (state, "not enough money"),
        (State::HasMoney(_), Input::Refund) => (State::Idle, "money returned"),
        (State::Dispensing, Input::Done) => (State::Idle, "enjoy!"),
        (_, _) => (state, "ignored"),
    }
}

fn main() {
    let inputs = [
        Input::PressButton,
        Input::InsertCoin(100),
        Input::PressButton,
        Input::InsertCoin(100),
        Input::PressButton,
        Input::InsertCoin(50),
        Input::Done,
    ];
    let mut state = State::Idle;
    for input in inputs {
        let (next, message) = transition(state, input);
        println!("{:<14} + {:<15} -> {:<14} {message}", format!("{:?}", state), format!("{:?}", input), format!("{:?}", next));
        state = next;
    }
}
Output
Idle           + PressButton     -> Idle           ignored
Idle           + InsertCoin(100) -> HasMoney(100)  coin accepted
HasMoney(100)  + PressButton     -> HasMoney(100)  not enough money
HasMoney(100)  + InsertCoin(100) -> HasMoney(200)  coin accepted
HasMoney(200)  + PressButton     -> Dispensing     dispensing
Dispensing     + InsertCoin(50)  -> Dispensing     ignored
Dispensing     + Done            -> Idle           enjoy!

Matching on the tuple (state, input) checks both values at once. The final (_, _) arm handles every combination not listed above it.

25. Realistic example: a result type of your own

Before meeting the standard library's Result, it helps to see that it is just an enum. This validation returns either a value or a list of problems.

Rust
#[derive(Debug)]
enum Validated {
    Valid { username: String, age: u8 },
    Invalid(Vec<String>),
}

fn validate(username: &str, age: i32) -> Validated {
    let mut problems = Vec::new();
    if username.len() < 3 {
        problems.push(format!("username '{username}' is too short"));
    }
    if !(13..=120).contains(&age) {
        problems.push(format!("age {age} is out of range"));
    }
    if problems.is_empty() {
        Validated::Valid { username: username.to_string(), age: age as u8 }
    } else {
        Validated::Invalid(problems)
    }
}

fn main() {
    for (name, age) in [("ferris", 8), ("al", 30), ("x", 200), ("grace", 45)] {
        match validate(name, age) {
            Validated::Valid { username, age } => println!("welcome {username} ({age})"),
            Validated::Invalid(problems) => println!("rejected: {}", problems.join("; ")),
        }
    }
}
Output
rejected: age 8 is out of range
rejected: username 'al' is too short
rejected: username 'x' is too short; age 200 is out of range
welcome grace (45)

26. Realistic example: pricing rules as data

Rules stored as enum values can be loaded from configuration, printed, and tested, instead of being hard-coded in if chains.

Rust
#[derive(Debug)]
enum Discount {
    None,
    Percent(u8),
    FixedCents(u64),
    BuyXGetY { buy: u32, free: u32 },
}

fn price_cents(unit_cents: u64, qty: u32, rule: &Discount) -> u64 {
    let full = unit_cents * qty as u64;
    match rule {
        Discount::None => full,
        Discount::Percent(p) => full - full * *p as u64 / 100,
        Discount::FixedCents(off) => full.saturating_sub(*off),
        Discount::BuyXGetY { buy, free } => {
            let group = buy + free;
            let free_items = qty / group * free;
            unit_cents * (qty - free_items) as u64
        }
    }
}

fn main() {
    let rules = [
        Discount::None,
        Discount::Percent(15),
        Discount::FixedCents(2_000),
        Discount::BuyXGetY { buy: 2, free: 1 },
    ];
    for rule in &rules {
        println!("{:<32} 7 x 4.00 = {:>6.2}", format!("{:?}", rule), price_cents(400, 7, rule) as f64 / 100.0);
    }
}
Output
None                             7 x 4.00 =  28.00
Percent(15)                      7 x 4.00 =  23.80
FixedCents(2000)                 7 x 4.00 =   8.00
BuyXGetY { buy: 2, free: 1 }     7 x 4.00 =  20.00

27. Realistic example: a tiny instruction interpreter

A stack machine whose program is a list of enum values. This is how bytecode interpreters are structured.

Rust
#[derive(Debug, Clone, Copy)]
enum Op {
    Push(i64),
    Add,
    Mul,
    Dup,
    Swap,
    Print,
}

fn run(program: &[Op]) {
    let mut stack: Vec<i64> = Vec::new();
    for op in program {
        match *op {
            Op::Push(n) => stack.push(n),
            Op::Add => {
                let b = stack.pop().unwrap();
                let a = stack.pop().unwrap();
                stack.push(a + b);
            }
            Op::Mul => {
                let b = stack.pop().unwrap();
                let a = stack.pop().unwrap();
                stack.push(a * b);
            }
            Op::Dup => {
                let top = *stack.last().unwrap();
                stack.push(top);
            }
            Op::Swap => {
                let n = stack.len();
                stack.swap(n - 1, n - 2);
            }
            Op::Print => println!("{:?}", stack),
        }
    }
}

fn main() {
    // computes (3 + 4) * (3 + 4) and 10 * 2
    let program = [
        Op::Push(3), Op::Push(4), Op::Add, Op::Dup, Op::Mul, Op::Print,
        Op::Push(2), Op::Push(10), Op::Swap, Op::Print, Op::Mul, Op::Print,
    ];
    run(&program);
}
Output
[49]
[49, 10, 2]
[49, 20]

match *op dereferences to copy the Op out, which works because Op is Copy. The unwrap() calls would panic on a malformed program; Result & the ? Operator shows how to report such errors properly.

Summary

  • An enum defines a type whose value is exactly one of several variants.
  • Variants can be plain names, tuple-like (Circle(f64)), or struct-like (Move { x: i32, y: i32 }).
  • match extracts the data and must handle every variant. Adding a variant turns every incomplete match into a compile error.
  • Enums have impl blocks, and derive works on them.
  • An enum is as large as its largest variant plus a tag. Recursive enums need a Box (or a Vec).
  • Enums model states, commands, events, messages, and results far more safely than strings or integer codes.

Next: Option: Replacing null, the most-used enum in Rust.