Chapter 6
Enums and Pattern Matching
This chapter looks at enumerations, also referred to as enums. Enums allow you to define a type by enumerating its possible variants. First we define and use an enum to show how an enum can encode meaning along with data. Next we explore a particularly useful enum, Option, which expresses that a value can be either something or nothing. Then we see how pattern matching in the match expression makes it easy to run different code for different values of an enum. Finally, we cover how the if let construct is another convenient and concise idiom for handling enums in your code.
- 6.1 Defining an Enum
- 6.2 The match Control Flow Construct
- 6.3 Concise Control Flow with if let and let...else
The Rust Programming Language · Ch.6 Enums and Pattern Matching
1
Chapter 6 · 6.1 Defining an Enum
Enums: a value is one of a set of possible values
Where structs give you a way of grouping together related fields and data, like a Rectangle with its width and height, enums give you a way of saying a value is one of a possible set of values. For example, we may want to say that Rectangle is one of a set of possible shapes that also includes Circle and Triangle. Rust lets us encode these possibilities as an enum.
Definition 6.1 (Enum). An enumeration: a type defined by enumerating its possible variants. A value of the enum is exactly one of those variants.
Why are enums more appropriate than structs in some cases? Say we need to work with IP addresses. Two major standards are currently used: version four and version six. Because these are the only possibilities our program will come across, we can enumerate all possible variants — which is where enumeration gets its name.
Any IP address can be either a version four or a version six address, but not both at the same time. That property makes the enum data structure appropriate, because an enum value can only be one of its variants. Both kinds are still fundamentally IP addresses, so they should be treated as the same type when the code handles situations that apply to any kind of IP address.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
2
Chapter 6 · 6.1 Defining an Enum
Defining the IpAddrKind enum
We can express this concept in code by defining an IpAddrKind enumeration and listing the possible kinds an IP address can be, V4 and V6. These are the variants of the enum:
enum IpAddrKind {
V4,
V6,
}
Definition 6.2 (Variant). One of the possible values listed in an enum definition, here V4 and V6. Variants are written inside the enum’s curly brackets, separated by commas.
IpAddrKind is now a custom data type that we can use elsewhere in our code.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
3
Chapter 6 · 6.1 Defining an Enum
Enum values
We can create instances of each of the two variants of IpAddrKind like this:
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
The variants of the enum are namespaced under its identifier, and we use a double colon :: to separate the two. This is useful because now both values IpAddrKind::V4 and IpAddrKind::V6 are of the same type: IpAddrKind.
We can then, for instance, define a function that takes any IpAddrKind:
fn route(ip_kind: IpAddrKind) {}
And we can call this function with either variant:
route(IpAddrKind::V4);
route(IpAddrKind::V6);
The Rust Programming Language · Ch.6 Enums and Pattern Matching
4
Chapter 6 · 6.1 Defining an Enum
Storing the address data: a struct attempt
Using enums has even more advantages. At the moment we have no way to store the actual IP address data; we only know what kind it is. Having just learned about structs in Chapter 5, you might be tempted to tackle this problem with structs, as in Listing 6.1.
Listing 6.1: Storing the data and IpAddrKind variant of an IP address using a struct
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
The Rust Programming Language · Ch.6 Enums and Pattern Matching
5
Chapter 6 · 6.1 Defining an Enum
Putting data directly into enum variants
In Listing 6.1 the struct IpAddr has two fields: kind, of type IpAddrKind (the enum defined previously), and address, of type String. The instance home has IpAddrKind::V4 as its kind with associated address data 127.0.0.1; loopback has the other variant, V6, with address ::1. The struct bundles the kind and address values together, so the variant is now associated with the value.
However, representing the same concept using just an enum is more concise: rather than an enum inside a struct, we can put data directly into each enum variant. This new definition of the IpAddr enum says that both V4 and V6 variants have associated String values.
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
The Rust Programming Language · Ch.6 Enums and Pattern Matching
6
Chapter 6 · 6.1 Defining an Enum
Variant constructors, and different data per variant
We attach data to each variant of the enum directly, so there is no need for an extra struct. It is also easier to see another detail of how enums work: the name of each variant we define also becomes a function that constructs an instance of the enum.
Definition 6.3 (Variant constructor). The function each variant name becomes. IpAddr::V4() is a function call that takes a String argument and returns an instance of the IpAddr type. We get it automatically as a result of defining the enum.
Another advantage over a struct: each variant can have different types and amounts of associated data. Version four addresses always have four numeric components with values between 0 and 255. To store V4 addresses as four u8 values but still express V6 addresses as one String, a struct would not do. Enums handle this case with ease:
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
The Rust Programming Language · Ch.6 Enums and Pattern Matching
7
Chapter 6 · 6.1 Defining an Enum
How the standard library defines IpAddr
Wanting to store IP addresses and encode which kind they are is so common that the standard library has a definition we can use (std::net::IpAddr). It has the exact enum and variants we defined and used, but it embeds the address data inside the variants in the form of two different structs, defined differently for each variant:
struct Ipv4Addr {
// --snip--
}
struct Ipv6Addr {
// --snip--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
You can put any kind of data inside an enum variant: strings, numeric types, or structs, for example. You can even include another enum! Also, standard library types are often not much more complicated than what you might come up with.
Remark. Even though the standard library contains a definition for IpAddr, we can still create and use our own definition without conflict, because we haven’t brought the standard library’s definition into our scope. Bringing types into scope is covered in Chapter 7.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
8
Chapter 6 · 6.1 Defining an Enum
The Message enum: varied data in each variant
Listing 6.2 shows another enum, with a wide variety of types embedded in its variants.
Listing 6.2: A Message enum whose variants each store different amounts and types of values
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
This enum has four variants with different types:
- Quit: has no data associated with it at all
- Move: has named fields, like a struct does
- Write: includes a single String
- ChangeColor: includes three i32 values
Defining an enum with variants such as these is similar to defining different kinds of struct definitions, except the enum doesn’t use the struct keyword and all the variants are grouped together under the Message type.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
9
Chapter 6 · 6.1 Defining an Enum
One enum versus several structs
The following structs could hold the same data that the variants of the Message enum hold:
struct QuitMessage; // unit struct
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct
But if we used the different structs, each of which has its own type, we couldn’t as easily define a function to take any of these kinds of messages as we could with the Message enum defined in Listing 6.2, which is a single type.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
10
Chapter 6 · 6.1 Defining an Enum
Defining methods on enums with impl
There is one more similarity between enums and structs: just as we can define methods on structs using impl, we can also define methods on enums. Here is a method named call that we could define on our Message enum:
impl Message {
fn call(&self) {
// method body would be defined here
}
}
let m = Message::Write(String::from("hello"));
m.call();
The body of the method would use self to get the value that we called the method on. Here we created a variable m that has the value Message::Write(String::from("hello")), and that is what self will be in the body of the call method when m.call() runs.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
11
Chapter 6 · 6.1 Defining an Enum
The Option enum: something or nothing
Another very common and useful enum defined by the standard library is Option; this case study explores it. The Option type encodes the very common scenario in which a value could be something, or it could be nothing.
For example, if you request the first item in a non-empty list, you get a value; if you request the first item in an empty list, you get nothing. Expressing this concept in terms of the type system means the compiler can check whether you’ve handled all the cases you should be handling — which can prevent bugs that are extremely common in other programming languages.
Programming language design is often thought of in terms of which features you include, but the features you exclude are important too. Rust doesn’t have the null feature that many other languages have.
Definition 6.4 (Null). A value that means there is no value there. In languages with null, variables can always be in one of two states: null or not-null.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
12
Chapter 6 · 6.1 Defining an Enum
The billion-dollar mistake
In his 2009 presentation “Null References: The Billion Dollar Mistake,” Tony Hoare, the inventor of null, had this to say:
I call it my billion-dollar mistake. At that time, I was designing the first comprehensive type system for references in an object-oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
The problem with null values is that if you try to use a null value as a not-null value, you’ll get an error of some kind. Because this null or not-null property is pervasive, it’s extremely easy to make this kind of error.
However, the concept that null is trying to express is still a useful one: a null is a value that is currently invalid or absent for some reason. The problem isn’t really with the concept but with the particular implementation.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
13
Chapter 6 · 6.1 Defining an Enum
Option<T>: Rust’s replacement for null
Rust does not have nulls, but it does have an enum that can encode the concept of a value being present or absent. This enum is Option<T>, and the standard library defines it as follows:
enum Option<T> {
None,
Some(T),
}
Definition 6.5 (Option<T>). The standard library enum whose variant Some(T) holds a present value of type T and whose variant None means the value is absent.
Option<T> is so useful that it’s even included in the prelude; you don’t need to bring it into scope explicitly. Its variants are also included in the prelude: you can use Some and None directly without the Option:: prefix. It is still just a regular enum, and Some(T) and None are still variants of type Option<T>.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
14
Chapter 6 · 6.1 Defining an Enum
The <T> type parameter and type inference
The <T> syntax is a generic type parameter, covered in Chapter 10. For now, all you need to know is that <T> means the Some variant of the Option enum can hold one piece of data of any type, and that each concrete type used in place of T makes the overall Option<T> type a different type. Some examples of Option values holding number types and char types:
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
The type of some_number is Option<i32>. The type of some_char is Option<char>, a different type. Rust can infer these types because we’ve specified a value inside the Some variant. For absent_number, Rust requires us to annotate the overall Option type: the compiler can’t infer the type that the corresponding Some variant will hold by looking only at a None value. Here we tell Rust that absent_number is of type Option<i32>.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
15
Chapter 6 · 6.1 Defining an Enum
Why Option<T> is better than null
When we have a Some value, we know that a value is present, and the value is held within the Some. When we have a None value, in some sense it means the same thing as null: we don’t have a valid value. So why is having Option<T> any better than having null?
In short, because Option<T> and T (where T can be any type) are different types, the compiler won’t let us use an Option<T> value as if it were definitely a valid value. For example, this code won’t compile, because it tries to add an i8 to an Option<i8>:
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
}
If we run this code, we get the error message shown on the next slide.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
16
Chapter 6 · 6.1 Defining an Enum
Error E0277: cannot add Option<i8> to i8
Running the code gives an error message like this one (the notes listing other Add implementations are elided):
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
help: the following other types implement trait `Add<Rhs>`
...
= note: this error originates in the macro `add_impl` (in Nightly builds,
run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` (bin "enums") due to 1 previous error
The Rust Programming Language · Ch.6 Enums and Pattern Matching
17
Chapter 6 · 6.1 Defining an Enum
What the error means: opting in to “maybe absent”
Intense! In effect, this error message means that Rust doesn’t understand how to add an i8 and an Option<i8>, because they’re different types. When we have a value of a type like i8, the compiler ensures that we always have a valid value. We can proceed confidently without having to check for null before using that value. Only when we have an Option<i8> (or whatever type of value we’re working with) do we have to worry about possibly not having a value — and the compiler makes sure we handle that case before using the value.
In other words, you have to convert an Option<T> to a T before you can perform T operations with it. Generally, this helps catch one of the most common issues with null: assuming that something isn’t null when it actually is.
- Eliminating the risk of incorrectly assuming a not-null value helps you be more confident in your code.
- To have a value that can possibly be null, you must explicitly opt in by making its type Option<T>; when you use that value, you are required to explicitly handle the case when the value is null.
- Everywhere a value has a type that isn’t an Option<T>, you can safely assume that the value isn’t null — a deliberate design decision to limit null’s pervasiveness and increase the safety of Rust code.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
18
Chapter 6 · 6.1 Defining an Enum
Getting the T out of Some
So how do you get the T value out of a Some variant when you have a value of type Option<T>, so that you can use that value?
- Option<T> has a large number of methods that are useful in a variety of situations; check them out in its documentation (std::option::Option). Becoming familiar with them will be extremely useful in your journey with Rust.
- In general, to use an Option<T> value you want code that handles each variant: some code that runs only when you have a Some(T) value, and is allowed to use the inner T; and some other code that runs only if you have a None value, which has no T value available.
Remark. The match expression is a control flow construct that does just this when used with enums: it runs different code depending on which variant of the enum it has, and that code can use the data inside the matching value. It is the subject of Section 6.2.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
19
Chapter 6 · 6.2 The match Control Flow Construct
match: comparing a value against patterns
Definition 6.6 (match). An extremely powerful control flow construct that compares a value against a series of patterns and then executes code based on which pattern matches.
Patterns can be made up of literal values, variable names, wildcards, and many other things; Chapter 19 covers all the different kinds of patterns and what they do. The power of match comes from the expressiveness of the patterns and the fact that the compiler confirms that all possible cases are handled.
Think of a match expression as being like a coin-sorting machine: coins slide down a track with variously sized holes along it, and each coin falls through the first hole it encounters that it fits into. In the same way, values go through each pattern in a match, and at the first pattern the value “fits,” the value falls into the associated code block to be used during execution.
Speaking of coins, let’s use them as an example! We can write a function that takes an unknown US coin and, in a similar way as the counting machine, determines which coin it is and returns its value in cents, as shown in Listing 6.3.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
20
Chapter 6 · 6.2 The match Control Flow Construct
Sorting coins with match
Listing 6.3: An enum and a match expression that has the variants of the enum as its patterns
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
Let’s break down the match in value_in_cents. First we list the match keyword followed by an expression, which in this case is the value coin.
This seems very similar to a conditional expression used with if, but there’s a big difference: with if, the condition needs to evaluate to a Boolean value, but here it can be any type. The type of coin in this example is the Coin enum that we defined on the first line.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
21
Chapter 6 · 6.2 The match Control Flow Construct
Match arms and how a match executes
Definition 6.7 (Match arm). One branch of a match, with two parts: a pattern and some code, separated by the => operator. Each arm is separated from the next with a comma.
In Listing 6.3 the first arm has a pattern that is the value Coin::Penny, then the => operator that separates the pattern and the code to run. The code in this case is just the value 1.
When the match expression executes, it compares the resultant value against the pattern of each arm, in order. If a pattern matches the value, the code associated with that pattern is executed. If that pattern doesn’t match the value, execution continues to the next arm, much as in a coin-sorting machine. We can have as many arms as we need: in Listing 6.3, our match has four arms.
The code associated with each arm is an expression, and the resultant value of the expression in the matching arm is the value that gets returned for the entire match expression.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
22
Chapter 6 · 6.2 The match Control Flow Construct
Multiple lines of code in a match arm
We don’t typically use curly brackets if the match arm code is short, as it is in Listing 6.3, where each arm just returns a value.
If you want to run multiple lines of code in a match arm, you must use curly brackets, and the comma following the arm is then optional.
For example, this code prints “Lucky penny!” every time the method is called with a Coin::Penny, but it still returns the last value of the block, 1.
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
The Rust Programming Language · Ch.6 Enums and Pattern Matching
23
Chapter 6 · 6.2 The match Control Flow Construct
Patterns that bind to values
Another useful feature of match arms is that they can bind to the parts of the values that match the pattern. This is how we can extract values out of enum variants.
Listing 6.4: A Coin enum in which the Quarter variant also holds a UsState value
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
As an example, let’s change one of our enum variants to hold data inside it. From 1999 through 2008, the United States minted quarters with different designs for each of the 50 states on one side. No other coins got state designs, so only quarters have this extra value. We add this information to our enum by changing the Quarter variant to include a UsState value stored inside it.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
24
Chapter 6 · 6.2 The match Control Flow Construct
Binding the state inside Coin::Quarter
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {state:?}!");
25
}
}
}
Imagine a friend is trying to collect all 50 state quarters. While we sort our loose change by coin type, we also call out the name of the state associated with each quarter, so that if it’s one our friend doesn’t have, they can add it to their collection.
In the match expression, we add a variable called state to the pattern that matches values of the variant Coin::Quarter. When a Coin::Quarter matches, the state variable binds to the value of that quarter’s state. Then we can use state in the code for that arm.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
25
Chapter 6 · 6.2 The match Control Flow Construct
Tracing a binding: an Alaska quarter
The program calls value_in_cents with a quarter from Alaska:
fn main() {
value_in_cents(Coin::Quarter(UsState::Alaska));
}
- If we call value_in_cents(Coin::Quarter(UsState::Alaska)), coin would be Coin::Quarter(UsState::Alaska).
- When we compare that value with each of the match arms, none of them match until we reach Coin::Quarter(state).
- At that point, the binding for state will be the value UsState::Alaska.
- We can then use that binding in the println! expression, thus getting the inner state value out of the Coin enum variant for Quarter.
Example 6.8. Because UsState derives Debug, the {state:?} placeholder prints the variant name: this call prints State quarter from Alaska! and returns 25.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
26
Chapter 6 · 6.2 The match Control Flow Construct
The Option<T> match pattern
Earlier we wanted to get the inner T value out of the Some case when using Option<T>. We can handle Option<T> using match, as we did with the Coin enum! Instead of comparing coins, we compare the variants of Option<T>, but the way the match expression works remains the same.
Say we want a function that takes an Option<i32> and, if there’s a value inside, adds 1 to that value. If there isn’t a value inside, the function should return the None value and not attempt to perform any operations. Thanks to match, this is very easy to write.
Listing 6.5: A function that uses a match expression on an Option<i32>
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
The Rust Programming Language · Ch.6 Enums and Pattern Matching
27
Chapter 6 · 6.2 The match Control Flow Construct
Tracing plus_one(five)
Let’s examine the first execution of plus_one in more detail. When we call plus_one(five), the variable x in the body of plus_one has the value Some(5). We then compare that against each match arm, starting with the first:
None => None,
The Some(5) value doesn’t match the pattern None, so we continue to the next arm:
Some(i) => Some(i + 1),
Does Some(5) match Some(i)? It does! We have the same variant. The i binds to the value contained in Some, so i takes the value 5. The code in the match arm is then executed, so we add 1 to the value of i and create a new Some value with our total 6 inside.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
28
Chapter 6 · 6.2 The match Control Flow Construct
Tracing plus_one(None)
Now consider the second call of plus_one in Listing 6.5, where x is None. We enter the match and compare to the first arm:
None => None,
It matches! There’s no value to add to, so the program stops and returns the None value on the right side of =>. Because the first arm matched, no other arms are compared.
Remark. Combining match and enums is useful in many situations. You’ll see this pattern a lot in Rust code: match against an enum, bind a variable to the data inside, and then execute code based on it. It’s a bit tricky at first, but once you get used to it, you’ll wish you had it in all languages. It’s consistently a user favorite.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
29
Chapter 6 · 6.2 The match Control Flow Construct
Matches are exhaustive: a buggy plus_one
There’s one other aspect of match we need to discuss: the arms’ patterns must cover all possibilities. Consider this version of our plus_one function, which has a bug and won’t compile:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
We didn’t handle the None case, so this code will cause a bug. Luckily, it’s a bug Rust knows how to catch. If we try to compile this code, we get the error shown on the next slide.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
30
Chapter 6 · 6.2 The match Control Flow Construct
Error E0004: non-exhaustive patterns
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
|
note: `Option<i32>` defined here
...
= note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard
pattern or an explicit pattern as shown
|
4 ~ Some(i) => Some(i + 1),
5 ~ None => todo!(),
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error
The Rust Programming Language · Ch.6 Enums and Pattern Matching
31
Chapter 6 · 6.2 The match Control Flow Construct
Exhaustiveness prevents the billion-dollar mistake
Rust knows that we didn’t cover every possible case and even knows which pattern we forgot: the error points at match x, reports that pattern None is not covered, and suggests adding an arm such as None => todo!().
Definition 6.9 (Exhaustive). Matches in Rust are exhaustive: the arms must exhaust every last possibility in order for the code to be valid.
Especially in the case of Option<T>, when Rust prevents us from forgetting to explicitly handle the None case, it protects us from assuming that we have a value when we might have null — making the billion-dollar mistake discussed earlier impossible.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
32
Chapter 6 · 6.2 The match Control Flow Construct
Catch-all patterns: a dice game
Using enums, we can also take special actions for a few particular values, but for all other values take one default action. Imagine a game where, if you roll a 3, your player doesn’t move but instead gets a fancy new hat. If you roll a 7, your player loses a fancy hat. For all other values, your player moves that number of spaces on the game board. Here is a match implementing that logic, with the dice roll hardcoded rather than random, and all other logic represented by functions without bodies, because implementing them is out of scope:
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
For the first two arms, the patterns are the literal values 3 and 7. For the last arm, which covers every other possible value, the pattern is the variable we’ve chosen to name other. The code that runs for the other arm uses the variable by passing it to the move_player function.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
33
Chapter 6 · 6.2 The match Control Flow Construct
Catch-alls go last; the _ placeholder
This code compiles, even though we haven’t listed all the possible values a u8 can have, because the last pattern matches all values not specifically listed. This catch-all pattern meets the requirement that match must be exhaustive.
Remark. We have to put the catch-all arm last, because the patterns are evaluated in order. If we put the catch-all arm earlier, the other arms would never run, so Rust warns us if we add arms after a catch-all!
Rust also has a pattern we can use when we want a catch-all but don’t want to use the value in the catch-all pattern:
Definition 6.10 (The _ pattern). _ is a special pattern that matches any value and does not bind to that value. It tells Rust we aren’t going to use the value, so Rust won’t warn us about an unused variable.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
34
Chapter 6 · 6.2 The match Control Flow Construct
Using _ when the value isn’t needed
Let’s change the rules of the game: now, if you roll anything other than a 3 or a 7, you must roll again. We no longer need to use the catch-all value, so we can change our code to use _ instead of the variable named other:
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
This example also meets the exhaustiveness requirement, because we’re explicitly ignoring all other values in the last arm; we haven’t forgotten anything.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
35
Chapter 6 · 6.2 The match Control Flow Construct
Doing nothing: the unit value in the _ arm
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
Finally, we change the rules one more time, so that nothing else happens on your turn if you roll anything other than a 3 or a 7. We express that by using the unit value (the empty tuple type from “The Tuple Type” in Chapter 3) as the code that goes with the _ arm.
Here we tell Rust explicitly that we aren’t going to use any other value that doesn’t match a pattern in an earlier arm, and we don’t want to run any code in this case.
Remark. There’s more about patterns and matching in Chapter 19. Next comes the if let syntax, which can be useful in situations where the match expression is a bit wordy.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
36
Chapter 6 · 6.3 Concise Control Flow with if let and let...else
A match that only cares about one variant
The if let syntax lets you combine if and let into a less verbose way to handle values that match one pattern while ignoring the rest. Consider the program in Listing 6.6, which matches on an Option<u8> value in the config_max variable but only wants to execute code if the value is the Some variant.
Listing 6.6: A match that only cares about executing code when the value is Some
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}
If the value is Some, we print out the value in the Some variant by binding the value to the variable max in the pattern. We don’t want to do anything with the None value. To satisfy the match expression, we have to add _ => () after processing just one variant, which is annoying boilerplate code to add.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
37
Chapter 6 · 6.3 Concise Control Flow with if let and let...else
The same logic with if let
Instead, we could write this in a shorter way using if let. The following code behaves the same as the match in Listing 6.6:
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}
Definition 6.11 (if let). A construct taking a pattern and an expression separated by an equal sign. It works the same way as a match where the expression is given to the match and the pattern is its first arm; its block runs only if the value matches the pattern.
Here the pattern is Some(max), and max binds to the value inside the Some. We can then use max in the body of the if let block in the same way we used max in the corresponding match arm. The code in the if let block only runs if the value matches the pattern.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
38
Chapter 6 · 6.3 Concise Control Flow with if let and let...else
match or if let? Conciseness versus exhaustiveness
Using if let means less typing, less indentation, and less boilerplate code. However, you lose the exhaustive checking that match enforces to ensure that you aren’t forgetting to handle any cases. Choosing between match and if let depends on what you’re doing in your particular situation and whether gaining conciseness is an appropriate trade-off for losing exhaustive checking.
Remark. In other words, you can think of if let as syntax sugar for a match that runs code when the value matches one pattern and then ignores all other values.
if let with else
We can include an else with an if let. The block of code that goes with the else is the same as the block of code that would go with the _ case in the match expression equivalent to the if let and else.
Recall Listing 6.4, where the Quarter variant also held a UsState value. Suppose we want to count all non-quarter coins we see while also announcing the state of the quarters.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
39
Chapter 6 · 6.3 Concise Control Flow with if let and let...else
Counting non-quarters: match versus if let ... else
We could do that with a match expression, like this:
let coin = Coin::Penny;
let mut count = 0;
match coin {
Coin::Quarter(state) => println!("State quarter from {state:?}!"),
_ => count += 1,
}
Or we could use an if let and else expression, like this:
let coin = Coin::Penny;
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {state:?}!");
} else {
count += 1;
}
The Rust Programming Language · Ch.6 Enums and Pattern Matching
40
Chapter 6 · 6.3 Concise Control Flow with if let and let...else
Staying on the “happy path”: a UsState helper
A common pattern is to perform some computation when a value is present and return a default value otherwise. Continuing with coins that hold a UsState value: if we wanted to say something funny depending on how old the state on the quarter was, we might introduce a method on UsState to check the age of a state, like so:
impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
// -- snip --
}
}
}
Then we might use if let to match on the type of coin, introducing a state variable within the body of the condition, as in Listing 6.7.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
41
Chapter 6 · 6.3 Concise Control Flow with if let and let...else
Work nested inside an if let
Listing 6.7: Checking whether a state existed in 1900 by using conditionals nested inside an if let
fn describe_state_quarter(coin: Coin) -> Option<String> {
if let Coin::Quarter(state) = coin {
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
} else {
None
}
}
That gets the job done, but it has pushed the work into the body of the if let statement; if the work is more complicated, it might be hard to follow exactly how the top-level branches relate. We could also take advantage of the fact that expressions produce a value — either to produce the state from the if let or to return early, as in Listing 6.8. (You could do something similar with a match, too.)
The Rust Programming Language · Ch.6 Enums and Pattern Matching
42
Chapter 6 · 6.3 Concise Control Flow with if let and let...else
Producing a value or returning early with if let
Listing 6.8: Using if let to produce a value or return early
fn describe_state_quarter(coin: Coin) -> Option<String> {
let state = if let Coin::Quarter(state) = coin {
state
} else {
return None;
};
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}
This is a bit annoying to follow in its own way, though! One branch of the if let produces a value, and the other one returns from the function entirely.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
43
Chapter 6 · 6.3 Concise Control Flow with if let and let...else
let...else: bind in the outer scope or leave
To make this common pattern nicer to express, Rust has let...else.
Definition 6.12 (let...else). A statement that takes a pattern on the left side and an expression on the right, very similar to if let, but with no if branch, only an else branch. If the pattern matches, it binds the value from the pattern in the outer scope. If the pattern does not match, the program flows into the else arm, which must return from the function.
Table 6.1: The three ways of matching seen in this chapter
| Construct | When the pattern matches | When it does not match |
|---|---|---|
| match | Runs the matching arm | Tries the next arm; every case must be covered |
| if let | Runs the block; bindings live inside it | Runs the else block, if any |
| let...else | Binds in the outer scope; code continues | Runs else, which must return |
Listing 6.9, on the next slide, shows how Listing 6.8 looks when using let...else in place of if let.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
44
Chapter 6 · 6.3 Concise Control Flow with if let and let...else
describe_state_quarter with let...else
Listing 6.9: Using let...else to clarify the flow through the function
fn describe_state_quarter(coin: Coin) -> Option<String> {
let Coin::Quarter(state) = coin else {
return None;
};
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}
Notice that it stays on the “happy path” in the main body of the function this way, without having significantly different control flow for two branches the way the if let did.
Remark. If your program has logic that is too verbose to express using a match, remember that if let and let...else are in your Rust toolbox as well.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
45
Chapter 6 · Summary
Chapter summary
In this chapter we covered:
- how to use enums to create custom types that can be one of a set of enumerated values;
- how the standard library’s Option<T> type helps you use the type system to prevent errors;
- when enum values have data inside them, how to use match or if let to extract and use those values, depending on how many cases you need to handle.
Your Rust programs can now express concepts in your domain using structs and enums. Creating custom types to use in your API ensures type safety: the compiler will make certain your functions only get values of the type each function expects.
Remark. Next, Chapter 7 turns to Rust’s modules, in order to provide a well-organized API to your users that is straightforward to use and only exposes exactly what your users will need.
The Rust Programming Language · Ch.6 Enums and Pattern Matching
46