Lecture 10 - Enums and Pattern Matching in Rust
Logistics
- Actitvity 9 solutions are posted on the site
- HW3 will be due next Thursday - the night before the exam
- Exam 1 is a week from Friday
- Format: similar to activities (a bit easier) and mid-lecture quizes - match/define, fill-in, find bugs, short answer, one short hand-coding problem
- No notes / reference sheets
- Monday will cover new material that will NOT be on the exam
- On Monday I will give you a final list of topics to review
- Wednesday will be a review session with practice problems
- HW2 is due TONIGHT at midnight
- I have office hours today and we'll answer questions on piazza until ~6pm
- Your submission is done when you've merged your work into main and then pushed to GitHub
- You can check to make everything looks good by navigating to GitHub.
Grading rubric
Homework grading
- On the syllabus: 1/3 correctness, 1/3 process, 1/3 style / best practices
- In practice: 1/2 autograder (passing tests), 1/2 qualitative review (by CAs and TAs)
How this intersects with the lateness and corrections policies
- If you push your last commit before the deadline, you get full credit. If you push again within the 48-hour late submission period, your grade will get scaled to 80%, and corrections can only bring you up to 80%.
- At the 48-hour mark you will be locked out and won't be able to push until after your homework is graded.
- After your homework is graded, you have one week to submit corrections:
- Corrections for correctness / test-passing can recover half-credit
- Corrections from feedback (the rubric) can recover full credit
- A week after the homework is initially graded, we will shut down editing for good and record final grades (if you made corrections).
Some examples
Person 1
- You submit your homework on time and get 100% on the autograder and 70% on the rubric, so your initial grade is 85%.
- You push a new version of your homework within a week of receiving your grade, accounting for all the feedback you received, making your final grade 100%.
Person 2
- You submit your homework 24 hours late and get 100% on the autograder and 90% on the rubric, so your initial grade is 95% * 0.8 = 76%.
- You are capped at 80% for turning the assignment in late and decide not to push corrections, making your final grade 76%.
Person 3
- You submit on time passing 6/10 tests for an autograder score of 60%, and get 70% on the rubric, so your initial grade is 65%.
- You make corrections to pass all tests and fix all points of feedback, improving your autograde score to 80% and rubric score to 100%, making your final grade 90%.
Grading rubric for HW2 (will be similar for future HWs)
- We will add a "code best practices" category later (including "idiomatic" Rust, error handling, efficiency/memory usage, ownership/borrowing) but we're not ready for it yet.

Learning Objectives
By the end of this lecture, students should be able to:
- Define custom enum types with variants and associated data
- Use
#[derive(Debug)]and#[derive(PartialEq)]for displaying and comparing enums - Use
matchstatements for exhaustive pattern matching on enums - Work with Rust's built-in
Option<T>andResult<T, E>enums
Enums
enumfor "enumeration"- allows you to define a type (like
i32orbool) by enumerating its possible variants - use
letto create instances of the enum variants
#![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 }
Using "use" as a shortcut
enum Direction {
North,
East,
South,
West,
SouthWest,
}
// Bring the variant `East` into scope
use Direction::East;
// Bringing two options into the current scope
use Direction::{South,West};
// we didn't have to specify "Direction::"
let dir_3 = East;
// Bringing all options in - THIS WON'T WORK IF THE ENUM IS IN THE SAME FILE
use Direction::*;
let dir_4 = North;
Using enums as parameters
We can also define a function that takes our new type as an argument.
fn turn(dir: Direction) { ... }
Displaying enums (#[derive(Debug)])
By default Rust doesn't know how to display a new enum type. We actually have to tell Rust we want to be able to do this first by adding the Debug "trait"
// #[derive(Debug)] enum Direction { North, East, South, West, } use Direction::*; fn main(){ let dir = North; println!("{:?}",dir); }
Comparing enums (#[derive(PartialEq)])
By default Rust doesn't know how to compare enum values for equality. We need to add the PartialEq "trait" to enable == and != comparisons.
// #[derive(PartialEq)] enum Direction { North, East, South, West, } use Direction::*; fn main(){ let dir1 = North; let dir2 = North; let dir3 = South; println!("{}", dir1 == dir2); println!("{}", dir1 != dir3); }
Control Flow with match
The match statement is used to control flow based on the value of an enum.
let dir = East;
match dir {
North => println!("N"),
South => println!("S"),
West => { // can do more than one thing
println!("Go west!");
println!("W")
}
East => println!("E"),
};
If we tried doing this with if/else statements it would have to look like:
// The ugly if/else version:
if dir.as_u8() == Direction::North.as_u8() {
println!("N");
} else if dir.as_u8() == Direction::East.as_u8() {
println!("E");
} else if dir.as_u8() == Direction::South.as_u8() {
println!("S");
} else if dir.as_u8() == Direction::West.as_u8() {
println!("Go west!");
println!("W");
} else {
// This should never happen, but compiler doesn't know that!
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 but doesn't do anything } }
WARNING - your catch-all has to go last or it'll gobble everything up!
match dir_2 {
_ => println!("anything else"),
// will never get here!!
North => println!("N"),
South => println!("S"),
}
Putting Data in an Enum Variant
- Each variant can come with additional information
#![allow(unused)] fn main() { #[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); } } let (a,b) = (9,3); // this is just short-hand for let a = 9; let b = 3; match divide(a,b) { DivisionResult::Answer(result) // assigns the variant value to result => println!("This result is {}",result), DivisionResult::DivisionByZero => println!("noooooo!!!!"), }; }
Variants with multiple values
#![allow(unused)] fn main() { enum DivisionResultWithRemainder { Answer(u32,u32), // Store the result of the integer division and the remainder DivisionByZero, } fn divide_with_remainder(x:u32, y:u32) -> DivisionResultWithRemainder { if y == 0 { DivisionResultWithRemainder::DivisionByZero } else { DivisionResultWithRemainder::Answer(x / y, x % y) // Return the integer division and the remainder } } let (a,b) = (9,4); match divide_with_remainder(a,b) { DivisionResultWithRemainder::Answer(result,remainder) => { println!("the result is {}",result); println!("the remainder is {}",remainder); } DivisionResultWithRemainder::DivisionByZero => println!("noooooo!!!!"), }; }
match as expression
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() { // swap east and west let mut 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); }
Beyond enums - pattern matching other types (FYI)
match works on more than just enums:
Matching tuples:
#![allow(unused)] 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:
#![allow(unused)] fn main() { let age = 25; match age { 0..=12 => println!("Child"), 13..=19 => println!("Teenager"), 20..=64 => println!("Adult"), 65.. => println!("Senior"), } }
Matching conditions:
#![allow(unused)] fn main() { let number = 42; match number { x if x % 2 == 0 => println!("{} is even", x), x => println!("{} is odd", x), } }
Destructuring arrays:
#![allow(unused)] 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"), } }
Quick Review: Lectures 7-9
1. Variables & Types (Lecture 7)
#![allow(unused)] fn main() { let x = 5; x = 10; // What happens here? }
- Works fine
- Compiler error
- Runtime error
2. Functions (Lecture 8)
#![allow(unused)] fn main() { fn calculate(a: i32, b: i32) -> i32 { a + b; } }
What does this function return?
- The sum of a and b
- The unit type ()
- A compiler error
3. Loops & Arrays (Lecture 9)
#![allow(unused)] fn main() { let arr = [1, 2, 3, 4, 5]; for (index, value) in arr.iter().enumerate() { if value % 2 == 0 { ________ } println!("Index: {}, Value: {}", index, value); } }
What goes in the blank to skip to next iteration without printing?
Enum Option<T>
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)
An Option<T> example
Here's example prime number finding code that returns Option<u32> if a prime number is found, or None if not.
If a prime number is found, it returns Some(u32) variant with the prime number.
If the prime number is not found, it returns None.
fn main(){ 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 } 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 with match
matchfn main() { let tmp : Option<u32> = Some(3); // let tmp: Option<u32> = None; match tmp { Some(x) => println!("A second way: {}",x), None => println!("None"), }; }
FYI - other ways to extract values (but we'll generally prefer match):
fn main() { let tmp : Option<u32> = Some(3); // let tmp: Option<u32> = None; if let Some(x) = tmp { println!("One way: {}",x); } println!("Another way: {}", tmp.unwrap()); // this will panic if tmp is None! }
Enum Option<T>: useful methods
Check the variant
.is_some() -> bool.is_none() -> bool
Get the value in Some or terminate with an error
.unwrap() -> T.expect(message) -> T
Get the value in Some or a default value
.unwrap_or(default_value:T) -> T
Enum Result<T, E>
We saw this one with guessing game. Another built-in enum with two variants:
Ok(T)Err(E)
Similar to Option except we have a value associated with both cases.
Useful when you want to pass a solution or information about an error.
For example:
#![allow(unused)] fn main() { fn divide_safely(x: f64, y: f64) -> Result<f64, String> { if y == 0.0 { Err("Cannot divide by zero!".to_string()) } else { Ok(x / y) } } }
Enum Result<T, E>: useful methods
Check the variant
.is_ok() -> bool.is_err() -> bool
Get the value in Ok or terminate with an error
.unwrap() -> T.expect(message) -> T
Get the value in Ok or a default value
.unwrap_or(default_value:T) -> T
Summary of Option<T> and Result<T,E>
Option<T>has variants that look likeSome(value of type T)andNoneResult<T,E>has variants that look likeOk(value of type T)andErr(error_info of type E)
Activity Time
Bonus - Simplified matching with if let (FYI)
We can't do a quick if on an enum like this:
enum Direction { North, East, South, West, } use Direction::*; fn main() { let dir: Direction = North; if dir == North { println!("North"); } }
Instead we would have to:
enum Direction { North, East, South, West, } use Direction::*; fn main() { let dir: Direction = North; match dir { North => println!("North"), _ => (), }; }
But this happens often enough that there's a shorthand:
enum Direction { North, East, South, West, } use Direction::*; fn main() { let dir: Direction = North; if let North = dir { // YES THIS LOOKS BACKWARDS! It's a more like match than if println!("North"); }; }
You can use else to match anything else like a regular if statement
if let North = dir {
println!("North");
} else {
println!("Going somewhere else");
};