Lecture 12 - Enums: variants, match, and exhaustiveness
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
matchthat 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
ifon amatcharm - 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); }
Debugis what{:?}needsPartialEqis 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 when | Reach for a struct when |
|---|---|
| The value is one of a few alternatives | The value has several parts, all at once |
| The cases carry different data, or none | Every one of them carries the same fields |
| You want the compiler to make you handle each case | You 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 struct | As an enum | |
|---|---|---|
| Temperature prefer struct |
|
|
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 variantSomecontains a value of typeTNone
Useful for when there may be no output
- Like
Noneornullin 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 amatchmatchas an expression, so an arm's value goes straight into alet- 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.