[The Rust Programming Language] 6. Enums and Pattern Matching

6. Enums and Pattern Matching

Enums allow you to define a type by enumerating its possible variants.

Rust’s enums are most similar to algebraic data types in functional languages, such as F#, OCaml, and Haskell.

6.1 Defining an Enum

Enum values can only be one of its variants.

1
2
3
4
5
6
7
8
9
10
11
12
enum IpAddrKind {
V4,
V6,
}

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

route(IpAddrKind::V4);
route(IpAddrKind::V6);

fn route(ip_kind: IpAddrKind) {}

We’ve used a struct to bundle the kind and address values together, so now the variant is associated with the value.

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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"),
};

We can represent the same concept in a more concise way using just an enum, rather than an enum inside a struct, by putting data directly into each enum variant.

1
2
3
4
5
6
7
8
enum IpAddr {
V4(String),
V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));

We attach data to each variant of the enum directly, so there is no need for an extra struct.

There’s another advantage to using an enum rather than a struct: each variant can have different types and amounts of associated data.

1
2
3
4
5
6
7
8
9
10
11
12
struct Ipv4Addr {
// --snip--
}

struct Ipv6Addr {
// --snip--
}

enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}

This code illustrates that 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.

There is one more similarity between enums and structs: just as we’re able to define methods on structs using impl, we’re also able to define methods on enums. Here’s a method named call that we could define on our Message enum:

1
2
3
4
5
6
7
8
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. In this example, we’ve 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 Option Enum and Its Advantages Over Null Values

This section explores a case study of Option, which is another enum defined by the standard library. The Option type is used in many places because it encodes the very common scenario in which a value could be something or it could be 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; this functionality can prevent bugs that are extremely common in other programming languages.

Rust doesn’t have the null feature that many other languages have. Null is 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.

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 it is defined by the standard library - https://doc.rust-lang.org/std/option/enum.Option.html as follows:

1
2
3
4
enum Option<T> {
Some(T),
None,
}

Here are some examples of using Option values to hold number types and string types:

1
2
3
4
let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

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.

The match expression is a control flow construct that does just this when used with enums: it will run different code depending on which variant of the enum it has, and that code can use the data inside the matching value.

6.2 The match Control Flow Operator

Rust has an extremely powerful control flow operator called match that allows you to compare a value against a series of patterns and then execute code based on which pattern matches. Patterns can be made up of literal values, variable names, wildcards, and many other things; Chapter 18 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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,
}
}

If you want to run multiple lines of code in a match arm, you can use curly brackets. For example, the following code would print “Lucky penny!” every time the method was called with a Coin::Penny but would still return the last value of the block, 1:

1
2
3
4
5
6
7
8
9
10
11
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

Patterns that Bind to Values

In the match expression for this code, 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 will bind to the value of that quarter’s state. Then we can use state in the code for that arm, like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}

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
}
}
}

If we were to 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.

Matching with Option

We can also handle Option using match as we did with the Coin enum! Instead of comparing coins, we’ll compare the variants of Option, but the way that the match expression works remains the same.

1
2
3
4
5
6
7
8
9
10
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); // Doesn’t match the pattern None, so we continue to the next arm.
let none = plus_one(None); // 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.

Matches Are Exhaustive

Consider this version of our plus_one function that has a bug and won’t compile:

This code does not compile!

1
2
3
4
5
fn plus_one(x: Option<i32>) -> Option<i32> {
match x { // error[E0004]: non-exhaustive patterns: `None` not covered
Some(i) => Some(i + 1),
}
}

Rust knows that we didn’t cover every possible case and even knows which pattern we forgot! Matches in Rust are exhaustive: we must exhaust every last possibility in order for the code to be valid. Especially in the case of Option, 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, thus making the billion-dollar mistake discussed earlier impossible.

The _ Placeholder

The _ pattern will match any value. By putting it after our other arms, the _ will match all the possible cases that aren’t specified before it. The () is just the unit value, so nothing will happen in the _ case. As a result, we can say that we want to do nothing for all the possible values that we don’t list before the _ placeholder.

1
2
3
4
5
6
7
8
let some_u8_value = 0u8;
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),
}

However, the match expression can be a bit wordy in a situation in which we care about only one of the cases. For this situation, Rust provides if let.

More about patterns and matching can be found in chapter 18 - https://doc.rust-lang.org/book/ch18-00-patterns.html.

6.3 Concise Control Flow with if let

he 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.

Using match:

1
2
3
4
5
let some_u8_value = Some(0u8);
match some_u8_value {
Some(3) => println!("three"),
_ => (),
}

Using `if let’:

1
2
3
4
let some_u8_value = Some(0u8);
if let Some(3) = some_u8_value {
println!("three");
}

Using if let means less typing, less indentation, and less boilerplate code. However, you lose the exhaustive checking that match enforces. 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.

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
// let mut count = 0;
// match coin {
// Coin::Quarter(state) => println!("State quarter from {:?}!", state),
// _ => count += 1,
// }

// Using if let:
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}

References

[1] The Rust Programming Language - The Rust Programming Language - https://doc.rust-lang.org/book/title-page.html

[2] Enums and Pattern Matching - The Rust Programming Language - https://doc.rust-lang.org/book/ch06-00-enums.html