“Dad, why is my sister’s name Rose?”
“Because your mother loves roses.”
“Thanks, dad!”
“No problem, Rust enums.”
Dad is right, enums are the best. If you know the crippled form of enums in other languages (cough C), I'm so sorry for you. In Rust, they are a pure delight to work with.
An enum is a type whose value is one of a fixed set of variants. Think of it
as a "this or that or that" type.
enum HttpStatus {
Ok,
NotFound,
InternalServerError,
}
You typically inspect an enum value with match. This is what I like about
match: the compiler checks that you've handled every variant. When you add a
new variant later, it points you to each match that no longer covers every
case.
fn code(status: HttpStatus) -> u16 {
match status {
HttpStatus::Ok => 200,
HttpStatus::NotFound => 404,
HttpStatus::InternalServerError => 500,
}
}
Each arm of a match is pattern => expression. Multiple patterns can share an
arm with |, and the catch-all is _:
match code {
200 | 201 | 204 => "success",
404 => "missing",
_ => "something else",
}
#[derive(...)]: Free ImplementationsYou'll see this line on many types in Rust:
#[derive(Debug, PartialEq)]
enum HttpStatus {
Ok,
NotFound,
InternalServerError,
}
The #[...] syntax is an attribute: extra instructions for the compiler
attached to the item below. derive is the most common one. It says "please
write the boilerplate for these capabilities for me." Each name inside the
parentheses is a trait, Rust's name for a shared interface, similar to a Java
interface or a Haskell type class. Traits are covered in more detail later.
For now, you need two:
Debug lets you print the value with the {:?} formatter, so
println!("{status:?}") prints NotFound instead of refusing to compile. You
can also use it in dbg!, assert_eq! failure messages, and quick log lines.PartialEq generates == and !=. Without it, comparing two HttpStatus
values is a compile error; with it, status == HttpStatus::Ok just works, and
assert_eq! in tests can compare whole enum values.Derive works on enums and structs whose fields all implement the same traits.
For PartialEq on an enum, the generated implementation considers two values
equal when they have the same variant and equal payloads. You can always write
the implementation by hand instead when you need different behaviour.
Write a match that turns each HttpStatus variant into the numeric code it
represents. If you forget one, the compiler points to the incomplete match
before the program can run.
After the tests pass, imagine adding TooManyRequests to the enum without
changing your match. Will it compile? Predict the result, then try it and
remove the extra variant. Would a _ catch-all hide the missing mapping?
Useful Resources
- The Rust Book on
matchexplains matching enum variants and handling every case.
// `Copy` lets you pass the same `HttpStatus` value to multiple functions
// without it being moved on the first call. Plain enums like this one (no
// `String`, no `Vec`, no other heap data) are always cheap to copy, so deriving
// `Copy` (and `Clone`) costs you nothing and removes a borrow-checker speed
// bump.
#[derive(Debug, PartialEq, Clone, Copy)]
enum HttpStatus {
Ok,
NotFound,
Unauthorized,
InternalServerError,
BadRequest,
}
/// Returns the HTTP status code number for the given status.
fn status_code(status: HttpStatus) -> u16 {
match status {
HttpStatus::Ok => 200,
HttpStatus::NotFound => 404,
HttpStatus::Unauthorized => 401,
HttpStatus::InternalServerError => 500,
HttpStatus::BadRequest => 400,
}
}
#[test]
fn test_status_code() {
assert_eq!(status_code(HttpStatus::Ok), 200);
assert_eq!(status_code(HttpStatus::NotFound), 404);
assert_eq!(status_code(HttpStatus::Unauthorized), 401);
assert_eq!(status_code(HttpStatus::BadRequest), 400);
assert_eq!(status_code(HttpStatus::InternalServerError), 500);
}
Sometimes you only care about a single variant. You can still write a full
match with a _ catch-all arm, or you can reach for the matches! macro.
Both are idiomatic.
For this exercise, return true only for InternalServerError, and false for
every other variant.
Useful from the Standard Library
std::matches!expands to amatchthat returnstruefor the given pattern andfalseotherwise. You can write it asmatches!(status, HttpStatus::InternalServerError).PartialEqenables==, so the enum's derived implementation also lets you writestatus == HttpStatus::InternalServerError.
// `Copy` lets you pass the same `HttpStatus` value to multiple functions
// without it being moved on the first call. Plain enums like this one (no
// `String`, no `Vec`, no other heap data) are always cheap to copy, so deriving
// `Copy` (and `Clone`) costs you nothing and removes a borrow-checker speed
// bump.
#[derive(Debug, PartialEq, Clone, Copy)]
enum HttpStatus {
Ok,
NotFound,
Unauthorized,
InternalServerError,
BadRequest,
}
/// Returns `true` if the request should be retried.
///
/// Only retry on server errors, not client errors.
fn should_retry(status: HttpStatus) -> bool {
matches!(status, HttpStatus::InternalServerError)
}
#[test]
fn test_should_retry() {
assert_eq!(should_retry(HttpStatus::InternalServerError), true);
assert_eq!(should_retry(HttpStatus::NotFound), false);
assert_eq!(should_retry(HttpStatus::Unauthorized), false);
assert_eq!(should_retry(HttpStatus::BadRequest), false);
assert_eq!(should_retry(HttpStatus::Ok), false);
}
You defined an enum with a fixed set of variants, mapped each variant to a value
with a match, and used matches! to ask a yes/no question about a single
variant.
What We Learned
- An
enumis a "this or that or that" type. Each value is exactly one of its variants, and the compiler tracks which one.matchchecks a value against patterns top to bottom and runs the first arm that fits. Every arm ispattern => expression, and the wholematchis itself an expression that produces a value.matchis exhaustive: leave a variant unhandled and the compiler refuses to build. Add a new variant later and everymatchthat needs updating tells you exactly where.|lets multiple patterns share an arm (200 | 201 | 204 => ...), and_is the catch-all when you want to ignore the rest.- Derive
Debugwhen you want{:?}printing andPartialEqwhen you want==comparisons. DerivingClone, Copylets callers reuse a value after passing it by value;Copyrequires every field to beCopytoo.- For a single-variant check,
matches!(value, Variant)is the compact form;value == Variantworks equally well whenPartialEqis derived.
Extra practice to explore at your own pace. These chapters do not count toward course progress.