Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lecture 12 - Enums: variants, match, and exhaustiveness

One box fanning out into three alternatives, one of them selected

Announcements




Learning objectives

By the end of today, you should be able to:

  • Define an enum, including variants that carry data
  • Use #[derive(Debug)] and #[derive(PartialEq)] to print and compare them
  • Write a match that covers every variant, and say what the compiler does when you miss one
  • Choose between an enum and a struct for a piece of data
  • Read a pattern guard, an if on a match arm
  • Use Option<T> when you want to return a value or nothing

Monday's Customer wasn't perfect

struct Customer {
    name: String,
    age: u32,
    state: String,
    member: bool,
}

name is free text. Whatever a person types is a legal name, and no rule we could write would say otherwise.

state is not free text. There are fifty of them, more or less.

But String lets us write both fields the same way.

What does Alice pay?

#[derive(Debug)]
struct Customer {
    name: String,
    age: u32,
    state: String,
    member: bool,
}

fn shipping_cost(c: &Customer) -> f64 {
    if c.state == "NY" { 5.00 } else { 12.00 }
}

fn main() {
    let alice = Customer {
        name: "Alice Smith".to_string(),
        age: 25,
        state: "ny".to_string(),
        member: true,
    };

    println!("{} pays ${:.2}", alice.name, shipping_cost(&alice));
}

Spellings the compiler is happy with

state: "ny".to_string()
state: "New York".to_string()
state: "NY ".to_string()
state: "Nueva York".to_string()

They all compile, but only one matches the shipping_cost function.

Our mistake was that state isn't a free string, it's more like a dropdown menu.

A text box, or a dropdown

A text box accepts the typo. A dropdown does not offer it.

Rust lets you build the dropdown, as a type:

enum State {
    NY,
    MA,
    CA,
    // ... 47(ish) more
}

struct Customer {
    name: String,
    age: u32,
    state: State,      // not String
    member: bool,
}

The field no longer accepts text. It accepts one of the variants of State, and nothing else.

The typo is now a compile error

let alice = Customer {
    name: "Alice Smith".to_string(),
    age: 25,
    state: State::ny,  // won't compile
    member: true,
};
error[E0599]: no variant, associated function, or constant named `ny`
              found for enum `State` in the current scope
help: there is a variant with a similar name
   |
18 -         state: State::ny,
18 +         state: State::NY,

This is a good thing! We catch the issue early and don't accidentally charge the wrong thing.

So what is an Enum anyway?

enum is short for "enumeration" and allows you to define a type by enumerating its possible variants.

Let's make another one:

#![allow(unused)]
fn main() {
// define the enum and its variants
enum Direction {
    North,
    East,
    South,
    West,
    SouthWest,
}

// create instances of the enum variants
let dir_1 = Direction::North;   // dir is inferred to be of type Direction
let dir_2: Direction = Direction::South; // dir_2 is explicitly of type Direction
}

The enum declaration is defining our new type, so now a type called Direction exists, alongside i32, f64, bool, etc.

The let declarations are creating instances of the Direction type.

Using "use" as a shortcut

enum Direction {
    North,
    East,
    South,
    West,
    SouthWest,
}
// Bring the variant `East` into scope
use Direction::East;
// Bring two of them into scope
use Direction::{South, West};
// Bring all of them into scope
use Direction::*;

// we didn't have to specify "Direction::"
let dir_3 = East;
let dir_4 = North;

Teaching Rust two things about your enum

Out of the box, Rust will not print your enum and will not compare two of them. But you can insist.

// #[derive(Debug, PartialEq)]
enum Direction {
    North,
    East,
    South,
    West,
}
use Direction::*;

fn main(){
    let here = North;
    let there = South;

    println!("{:?}", here);
    println!("{}", here == there);
}
  • Debug is what {:?} needs
  • PartialEq is what == and != need

Control Flow with match

The match statement is used to control flow based on the value of an enum.

enum Direction {
    North,
    East,
    South,
    West,
}
use Direction::*;
fn report(dir: Direction) {
    match dir {
        North => println!("N"),
        South => println!("S"),
        West => {  // can do more than one thing
            println!("Go west!");
            println!("W")
        }
        East => println!("E"),
    };
}

fn main() {
    report(North);
    report(East);
    report(South);
    report(West);
}

dir is a parameter typed Direction, so the only things it can be are the four variants.

If we tried doing this with if/else statements it would have to look like:

// The ugly if/else version (and it needs the PartialEq derive):
if dir == North {
    println!("N");
} else if dir == East {
    println!("E");
} else if dir == South {
    println!("S");
} else if dir == West {
    println!("Go west!");
    println!("W");
} else {
    // Nothing here should ever run, but the compiler can't tell us that,
    // and it can't tell us if we forget one either
    unreachable!();
}

Covering all variants with match

match is exhaustive, so we must cover all the variants!

If we didn't...

enum Direction {
    North,
    East,
    South,
    West,
}
use Direction::*;
fn main() {
let dir_2: Direction = South;

match dir_2 {
    North => println!("N"),
    South => println!("S"),
    // East and West not covered
};
}

But there is a way to match anything left.

enum Direction {
    North,
    East,
    South,
    West,
}
use Direction::*;
fn main() {
let dir_2: Direction = Direction::North;

match dir_2 {
    North => println!("N"),
    South => println!("S"),
    
    // match anything left
    _ => (),  // covers all the other variants and doesn't do anything
}
}

WARNING - your catch-all has to go last or it'll gobble everything up!

Just like if-elseif-else... or a series of filters.

enum Direction {
    North,
    East,
    South,
    West,
}
use Direction::*;
fn report(dir: Direction) {
    match dir {
        _ => println!("anything else"),

        // will never get here!!
        North => println!("N"),
        South => println!("S"),
    }
}

fn main() {
    report(North);
    report(East);
    report(South);
    report(West);
}

Every call prints anything else, not what you want. The warning helps here!

warning: unreachable pattern
   |
10 |         _ => println!("anything else"),
   |         - matches any value
13 |         North => println!("N"),
   |         ^^^^^ no value can reach this

Another quick example

enum Coin { Penny, Nickel, Dime }

fn value(c: Coin) -> u32 {
    match c {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
    }
}

fn main() {
    println!("{}", value(Coin::Penny));
    println!("{}", value(Coin::Dime));
}

Putting Data in an Enum Variant

Each variant can come with additional information

#[derive(Debug)] 
enum DivisionResult {
    Answer(u32), 
    DivisionByZero,
}

fn divide(x:u32, y:u32) -> DivisionResult {
    if y == 0 {
        return DivisionResult::DivisionByZero;
    } else {
        return DivisionResult::Answer(x / y); 
    }
}

fn main() {
    // each pass through the loop destructures one pair into a and b
    for (a,b) in [(9,3), (7,0)] {
        match divide(a,b) {
            DivisionResult::Answer(result)  // assigns the variant value to result
                => println!("This result is {}",result),
            DivisionResult::DivisionByZero
                => println!("noooooo!!!!"),
        };
    }
}
This result is 3
noooooo!!!!

So which one?

A struct has several parts, all at once.

An enum is one of a few alternatives.

Reach for an enum whenReach for a struct when
The value is one of a few alternativesThe value has several parts, all at once
The cases carry different data, or noneEvery one of them carries the same fields
You want the compiler to make you handle each caseYou want to add a field later without touching every match

Real code usually has both: a struct to group the fields, and an enum for the field that is one of a few choices.

Sometimes either one will work and you pick the one that fits better, sometimes you're forced into one.

The same type, written both ways

As a structAs an enum
Temperature
prefer struct
enum Scale { Fahrenheit, Celsius }
struct Temperature {
    degrees: f64,
    scale: Scale,
}
enum Temperature {
    Fahrenheit(f64),
    Celsius(f64),
}

Beyond enums: match works on other types

Matching on a condition, which you will see again in a minute:

fn main() {
    let number = 42;

    match number {
        x if x % 2 == 0 => println!("{} is even", x),
        x => println!("{} is odd", x),
    }
}

The if on an arm is a guard. That arm matches only when the pattern fits and the condition holds.

Other shapes, one line each. Full versions are on the website:

(0, y) => ...            // a tuple, keeping y
13..=19 => ...           // a range
[1, _, _] => ...         // an array starting with 1
Book { rating, .. } => ...  // a struct, keeping one field

The Option Enum

Find someone's last name

We've all written something like this in Python:

def find_index(target, names):
    for i in range(len(names)):
        if target == names[i]:
            return i
    return -1

index = find_index("Kesar", names)
print(last_names[index])

Now let's look up somebody who is not in the list

index = find_index("Totoro", names)  # Totoro is not in names
print(last_names[index])               # last_names[-1]

No error. No crash. It prints... what?

How Rust solves it elegantly

There is a built-in enum Option<T> with two variants:

  • Some(T) - The variant Some contains a value of type T
  • None

Useful for when there may be no output

  • Like None or null in other languages
  • Rust makes you explicitly handle them, preventing bugs that are extremely common in other languages
  • This might look a little like "optional" parameters in python (def myfn(arg: Optional[int] = None): but functions differently)

You write Direction::North with the enum's name in front, but plain Some and None with nothing in front. They are in the prelude, the short list of names Rust hands you for free in every file. Option::Some(3) works too, and nobody writes it.

An Option<T> example

Here is prime-finding code whose return type is Option<u32>.

If it finds a prime, it returns Some(u32) holding that prime.

If it does not, it returns None.

fn is_prime(x:u32) -> bool {
    if x <= 1 { return false;}
    for i in 2..=((x as f64).sqrt() as u32) {
        if x % i == 0 { return false; }
    } 
    true
}

fn prime_in_range(a:u32,b:u32) -> Option<u32> {  // returns an Option<u32>
    for i in a..=b {
        if is_prime(i) {return Some(i);}
    }
    None
}

fn main(){
    let tmp : Option<u32> = prime_in_range(90,906);
    // let tmp : Option<u32> =  prime_in_range(20,22);
    println!("{:?}",tmp);
}

Extracting the contents of an Option<T> with match

fn main() {
let tmp : Option<u32> = Some(3);
// let tmp: Option<u32> = None;

match tmp {
    Some(____) => println!("Got: {}",____),
    None => println!("None"),
};
}

How we do find_index, in Rust

fn find_index(target: &str, names: [&str; 3]) -> Option<usize> {
    for i in 0..names.len() {
        if target == names[i] {
            return Some(i);
        }
    }
    None
}

fn main() {
    let names = ["Kesar", "Mei", "Satsuki"];
    let last_names = ["Patel", "Kusakabe", "Kusakabe"];

    for target in ["Kesar", "Totoro"] {
        match find_index(target, names) {
            Some(i) => println!("{} {}", target, last_names[i]),
            None => println!("{} is not in the list", target),
        }
    }
}
Kesar Patel
Totoro is not in the list

There is no -1 to index with by accident. To get the number out you have to go through the match, and the match makes you say what happens when it is None.

The name on the left is one you make up

Same function, different names, same output:

fn main() {
    let number = 42;

    match number {
        tuesday if tuesday % 2 == 0 => println!("{} is even", tuesday),
        y => println!("{} is odd", y),
    }
}

x, y, tuesday. Rust knows none of these names before you write them. A pattern on the left of => creates a new variable, and it exists only inside that one arm. (You can even call it number again but that's confusing)

Same thing in Some(x). The x catches whatever was inside the Some.

let tmp: Option<u32> = Some(3);

match tmp {
    Some(x) => println!("{}", x),    // 3
    None => println!("nothing"),
}

Activity Time

Paper instructions, Playground for solving, Gradescope for submitting.

Lean on the compiler! (And lecture notes.)

Feel free to change the problems so they ask you to PRINT strings instead of return them.

These problems are harder than what we'd expect you to hand-code!

More match, on the website

Everything past this point is on the lecture page rather than on your paper. Six short appendices, if you want to go further:

  • Matching on a struct, and guarding an arm with if
  • if let, a shorthand for when you only care about one arm of a match
  • match as an expression, so an arm's value goes straight into a let
  • The other pattern shapes: tuples, ranges, and arrays
  • More destructuring: partial with .., and a pattern in a function parameter
  • Two more enums: the temperature types with their methods, and Project 1's SecretKeeper

Matching on a struct, and guarding an arm

match works on structs too. Name the fields you want, .. for the rest.

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Book {
    title: String,
    rating: f64,
    pages: u32,
}

fn get_rating(book: &Book) -> f64 {
    match book {
        &Book { rating, .. } => rating,
    }
}

fn classify(book: &Book) -> &str {
    match book {
        &Book { rating, pages, .. } if rating >= 4.5 && pages >= 400 => "Epic Bestseller",
        &Book { rating, .. } if rating >= 4.5 => "Highly Rated",
        _ => "Standard",
    }
}
}

Arms are tried top to bottom, so the fussiest guard goes first.

The & in &Book { rating, .. } is there because book is a &Book. The pattern is shaped like the type it matches. (And we'll understand exactly how it works... you guessed it, after the midterm.)

Bonus - Simplified matching with if let (FYI)

When you care about one case only, match makes you write a _ arm you have no use for:

enum Direction {
    North,
    East,
    South,
    West,
}
use Direction::*;
fn main() {
for dir in [North, East, South, West] {
    match dir {
        North => println!("this one is North"),
        _ => (),
    };
}
}

That happens often enough to have a shorthand:

enum Direction {
    North,
    East,
    South,
    West,
}
use Direction::*;
fn main() {
for dir in [North, East, South, West] {
    if let North = dir { // YES THIS LOOKS BACKWARDS! It's more like match than if
        println!("this one is North");
    }
}
}

You can add an else, the same as on a regular if.

You will meet it most often on Option, where the pattern carries a value. These two do the same thing:

fn main() {
    let tmp: Option<u32> = Some(3);

    // with match
    match tmp {
        Some(x) => println!("match: {}", x),
        None => println!("nothing"),
    }

    // with if let, when you only care about one of the two cases
    if let Some(x) = tmp {
        println!("if let: {}", x);
    }
}

if let reads backwards the first few times. Some(x) is the pattern and tmp is the value being tested against it, which is the same order a match arm uses. And unlike ==, it can reach inside a variant and pull the value out.

Appendix: match as an expression (FYI)

The result of a match can be used as an expression.

Each branch (arm) returns a value.

#[derive(Debug)]
enum Direction {
    North,
    East,
    South,
    West,
}
use Direction::*;
fn main() {
// turn left
let dir_facing = North;
println!("{:?}", dir_facing);

let after_turning_left = match dir_facing {
    North => West,
    West => South,
    South => East,
    East => North
};

println!("{:?}", after_turning_left);
}

Appendix: the other pattern shapes (FYI)

Matching tuples:

fn main() {
    let point = (3, 5);
    match point {
        (0, 0) => println!("Origin"),
        (0, y) => println!("On Y-axis at {}", y),
        (x, 0) => println!("On X-axis at {}", x),
        (x, y) => println!("Point at ({}, {})", x, y),
    }
}

Matching ranges:

fn main() {
    let age: u32 = 25;
    match age {
        0..=12 => println!("Child"),
        13..=19 => println!("Teenager"),
        20..=64 => println!("Adult"),
        65.. => println!("Senior"),
    }
}

Destructuring arrays:

fn main() {
    let arr = [1, 2, 3];
    match arr {
        [1, 2, 3] => println!("Exact match"),
        [1, _, _] => println!("Starts with 1"),
        [_, _, 3] => println!("Ends with 3"),
        _ => println!("Something else"),
    }
}

Appendix: more destructuring (FYI)

You have done this with tuples: let (x, y) = (3, 5);. A struct works the same way, naming every field: let Book { title, rating, pages } = dune;.

Partial destructuring with .., when you only want some of the fields:

struct Book { title: String, rating: f64, pages: u32 }

fn main() {
    let dune = Book { title: String::from("Dune"), rating: 4.6, pages: 412 };
    println!("rating: {}", dune.rating);

    // ignore rating this time
    let Book { title, pages, .. } = dune;
    println!("{} is {} pages", title, pages);
}

In a function parameter, so the body never mentions the struct at all:

fn print_pages(Book { title, pages, .. }: &Book) {
    println!("{} is {} pages", title, pages);
}

Appendix: the temperature types, and Project 1's SecretKeeper (FYI)

The two temperature types from the slide, with a sensor id added and the methods that use them. Both versions print identical output.

Temperature as a struct. Converting needs the scale; reading the sensor id does not.

enum Scale { Fahrenheit, Celsius }

struct Temperature {
    degrees: f64,
    scale: Scale,
    sensor_id: u32,
}

impl Temperature {
    // converting needs the scale
    fn in_celsius(&self) -> f64 {
        match self.scale {
            Scale::Celsius => self.degrees,
            Scale::Fahrenheit => (self.degrees - 32.0) * 5.0 / 9.0,
        }
    }
}

fn main() {
    let body = Temperature { degrees: 98.6, scale: Scale::Fahrenheit, sensor_id: 7 };
    let boiling = Temperature { degrees: 100.0, scale: Scale::Celsius, sensor_id: 4 };

    for t in [&body, &boiling] {
        // the sensor id has nothing to do with the scale, so just read the field
        println!("sensor {}: {:.1} C", t.sensor_id, t.in_celsius());
    }
}

Temperature as an enum. Same output, but reading the sensor id takes a match whose two arms do the same thing.

enum Temperature {
    Fahrenheit(f64, u32),   // degrees, sensor id
    Celsius(f64, u32),      // degrees, sensor id
}

impl Temperature {
    // converting needs the scale
    fn in_celsius(&self) -> f64 {
        match self {
            &Temperature::Celsius(degrees, _) => degrees,
            &Temperature::Fahrenheit(degrees, _) => (degrees - 32.0) * 5.0 / 9.0,
        }
    }

    // the sensor id has nothing to do with the scale, and we still have to ask
    fn sensor_id(&self) -> u32 {
        match self {
            &Temperature::Fahrenheit(_, id) => id,
            &Temperature::Celsius(_, id) => id,
        }
    }
}

fn main() {
    let body = Temperature::Fahrenheit(98.6, 7);
    let boiling = Temperature::Celsius(100.0, 4);

    for t in [&body, &boiling] {
        println!("sensor {}: {:.1} C", t.sensor_id(), t.in_celsius());
    }
}

Project 1's SecretKeeper. The one you saw on Monday was trimmed. Here is the field the real one has:

enum Source {
    Human,
    Known(u32),
}

pub struct SecretKeeper {
    source: Source,
    questions: u32,
    already_used: Vec<u32>,
}

There is no secret field. A keeper either asks the person at the keyboard, or it is already holding a number. Only one of those two carries a u32, so the number lives inside the variant.

Why not a secret field with a human flag next to it? Because then a Human keeper would still have a secret number sitting inside it, and nothing would stop you reading it. The enum makes the impossible combination impossible to write down: if it is Human, there is no number to read.