Enums & Pattern Matching 🎭
Structs say "this AND that." Enums say "this OR that."
Together with match, they end one of computing's most expensive
mistakes: the null pointer.
One of several possibilities
An enum (enumeration) is a type whose value is exactly one of a fixed set of variants:
enum Weather {
Sunny,
Cloudy,
Rainy,
Snowy,
}
fn main() {
let today = Weather::Rainy;
}
A Weather is never two of these, never none of these, never
"Purple": the compiler guarantees it. That already kills a whole species of bug
(the stray string: "rainy" vs "Rainy" vs "RAINY"…).
Variants can carry data 🎒
Here's where Rust enums leave other languages behind. Each variant can hold its own cargo:
enum Message {
Quit, // no data
Move { x: i32, y: i32 }, // struct-like data
Write(String), // one value
ChangeColor(u8, u8, u8), // three values
}
A Message is one shape or another, each with exactly the
data that shape needs. Modeling "it's this or that, with different details" is
most of programming; enums make it native.
match: the pattern-matching powerhouse
enum Weather {
Sunny,
Rainy,
Windy(u32), // wind speed in km/h
}
fn advice(w: Weather) -> String {
match w {
Weather::Sunny => String::from("Sunscreen. You're a crab."),
Weather::Rainy => String::from("You live in water. Proceed."),
Weather::Windy(speed) if speed > 80 => {
String::from("Grip the rock. GRIP THE ROCK.")
}
Weather::Windy(speed) => format!("Breezy at {speed} km/h. Enjoy."),
}
}
fn main() {
println!("{}", advice(Weather::Windy(95)));
}
Three superpowers on display:
- Destructuring:
Windy(speed)pulls the cargo out of the variant into a variable. - Guards:
if speed > 80adds a condition to an arm. - Exhaustiveness: delete the
Rainyarm and the program won't compile: "patternWeather::Rainynot covered." Add a new variant next year, and the compiler walks you to everymatchthat needs updating. That's refactoring with a safety net.
The _ pattern means "anything else". It's useful, but use it sparingly:
every _ is a place the compiler stops checking for you.
The billion-dollar bug, and Rust's answer 💸
Tony Hoare, who invented the null reference in 1965, calls it his "billion-dollar mistake": decades of crashes from programs using a value that turned out to be nothing. Most languages still live with it. Rust simply… doesn't have null.
Instead, "maybe a value" is an ordinary enum from the standard library:
enum Option<T> { // T = any type (generics, Lesson 12)
Some(T), // there IS a value, here it is
None, // there is no value
}
Anything that might be absent is an Option, and the compiler
forces you to handle both cases before you can touch the value:
fn find_nickname(name: &str) -> Option<String> {
if name == "Christopher" {
Some(String::from("Chris"))
} else {
None
}
}
fn main() {
match find_nickname("Christopher") {
Some(nick) => println!("Call them {nick}"),
None => println!("No nickname on file."),
}
}
You cannot forget the None case; it won't compile. The
null-pointer crash isn't "less likely" in Rust; it's grammatically impossible.
The one-case shortcut: if let
When you only care about one variant, a full match is ceremony.
if let is the shorthand:
let nickname = find_nickname("Christopher");
if let Some(nick) = nickname {
println!("Call them {nick}");
}
// (optionally: } else { ... } for the None side)
- Reaching for
.unwrap()to skip theNonecase: it crashes the program if the value is absent. Fine in toy code; in real code,match,if let, orunwrap_or(default). - Forgetting
Some(...)when returning:return nick;whereSome(nick)is needed. - Overusing
_arms: you're muting your best reviewer.
Traffic light
Create an enum Light with Red, Yellow,
Green. Write fn action(light: Light) -> &'static str
using match (that 'static is just "text built into the
program"; details in Lesson 13). Then try deleting one arm and read the error.
Reveal solution
enum Light {
Red,
Yellow,
Green,
}
fn action(light: Light) -> &'static str {
match light {
Light::Red => "stop",
Light::Yellow => "hurry up (kidding: slow down)",
Light::Green => "go",
}
}
fn main() {
println!("{}", action(Light::Yellow));
}
Safe division
Division by zero is nonsense. Model it! Write
fn divide(a: f64, b: f64) -> Option<f64> returning
None when b == 0.0. Call it twice (once with zero) and
match on both results.
Reveal solution
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}
fn main() {
for (a, b) in [(10.0, 4.0), (1.0, 0.0)] {
match divide(a, b) {
Some(result) => println!("{a} / {b} = {result}"),
None => println!("{a} / {b} is undefined, friend"),
}
}
}
Message dispatcher
Using the Message enum from above, write a function that takes a
Message and describes it, e.g. Move { x, y } prints
"moving to (x, y)". Create one of each variant and dispatch them all.
Reveal solution
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
fn dispatch(msg: Message) {
match msg {
Message::Quit => println!("goodbye!"),
Message::Move { x, y } => println!("moving to ({x}, {y})"),
Message::Write(text) => println!("writing: {text}"),
Message::ChangeColor(r, g, b) => println!("color: #{r:02x}{g:02x}{b:02x}"),
}
}
fn main() {
dispatch(Message::Move { x: 3, y: 7 });
dispatch(Message::Write(String::from("hi")));
dispatch(Message::ChangeColor(247, 76, 0));
dispatch(Message::Quit);
}