All the Places Patterns May be Used

Patterns pop up in a number of places in Rust. You’ve been using them a lot without realizing it! This section is a reference to all the places where patterns are valid.

match Arms

As we discussed in Chapter 6, a common place patterns are used is in the arms of match expressions. Formally, match expressions are defined as the keyword match, a value to match on, and one or more match arms that consist of a pattern and an expression to run if the value matches that arm’s pattern:

match VALUE {
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
    PATTERN => EXPRESSION,
}

Exhaustiveness and the Default Pattern _

match expressions are required to be exhaustive. When we put all of the patterns in the arms together, all possibilities for the value in the match expression must be accounted for. One way to ensure you have every possibility covered is to have a catch-all pattern for the last arm, like a variable name. A name matching any value can never fail and thus covers every case remaining after the previous arms’ patterns.

There’s an additional pattern that’s often used in the last match arm: _. It matches anything, but it never binds any variables. This can be useful when you only want to run code for some patterns but ignore any other value, for example.

if let Expressions

We discussed if let expressions in Chapter 6, and how they’re mostly a shorter way to write the equivalent of a match that only cares about matching one case. if let can optionally have a corresponding else with code to run if the pattern in the if let doesn’t match.

Listing 18-1 shows that it’s even possible to mix and match if let, else if, and else if let. This code shows a series of checks of a bunch of different conditions to decide what the background color should be. For the purposes of the example, we’ve created variables with hardcoded values that a real program might get by asking the user. If the user has specified a favorite color, we’ll use that as the background color. If today is Tuesday, the background color will be green. If the user has specified their age as a string and we can parse it as a number successfully, we’ll use either purple or orange depending on the value of the parsed number. Finally, if none of these conditions apply, the background color will be blue:

Filename: src/main.rs

fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(color) = favorite_color {
        println!("Using your favorite color, {}, as the background", color);
    } else if is_tuesday {
        println!("Tuesday is green day!");
    } else if let Ok(age) = age {
        if age > 30 {
            println!("Using purple as the background color");
        } else {
            println!("Using orange as the background color");
        }
    } else {
        println!("Using blue as the background color");
    }
}

Listing 18-1: Mixing if let, else if, else if let, and else

This conditional structure lets us support complex requirements. With the hardcoded values we have here, this example will print Using purple as the background color.

Note that if let can also introduce shadowed variables like match arms can: if let Ok(age) = age introduces a new shadowed age variable that contains the value inside the Ok variant. This also means the if age > 30 condition needs to go within the block; we aren’t able to combine these two conditions into if let Ok(age) = age && age > 30 since the shadowed age that we want to compare to 30 isn’t valid until the new scope starts with the curly brace.

Also note that conditionals with many cases like these are not as powerful as match expression since exhaustiveness is not checked by the compiler. If we leave off the last else block and miss handling some cases, the compiler will not error. This example might be too complex to rewrite as a readable match, so we should take extra care to check that we’re handling all the cases since the compiler is not checking exhaustiveness for us.

while let

A similar construction to if let is while let: this allows you to do a while loop as long as a pattern continues to match. Listing 18-2 shows an example using a while let loop to use a vector as a stack and print out the values in the vector in the opposite order that we pushed the values in:

let mut stack = Vec::new();

stack.push(1);
stack.push(2);
stack.push(3);

while let Some(top) = stack.pop() {
    println!("{}", top);
}

Listing 18-2: Using a while let loop to print out values as long as stack.pop() returns Some

This example will print 3, 2, then 1. The pop method takes the last element out of the vector and returns Some(value). If the vector is empty, it returns None. The while loop will continue running the code in its block as long as pop is returning Some. Once it returns None, the while loop stops. We can use while let to pop every element off our stack.

for loops

Looping with for, as we discussed in Chapter 3, is the most common loop construction in Rust code. What we didn’t talk about in that chapter was that for takes a pattern. In Listing 18-3, we’re demonstrating how we can use a pattern in a for loop to destructure a tuple. The enumerate method adapts an iterator to produce a value and the index of the value in the iterator in a tuple:

let v = vec![1, 2, 3];

for (index, value) in v.iter().enumerate() {
    println!("{} is at index {}", value, index);
}

Listing 18-3: Using a pattern in a for loop to destructure the tuple returned from enumerate into its pieces

This will print:

1 is at index 0
2 is at index 1
3 is at index 2

The first call to enumerate produces the tuple (0, 1). When this value is matched to the pattern (index, value), index will be 0 and value will be 1.

let Statements

match and if let are the places we’ve explicitly discussed using patterns earlier in the book, but they aren’t the only places we’ve used patterns. For example, consider this straightforward variable assignment with let:

let x = 5;

We’ve done this hundreds of times throughout this book. You may not have realized it, but you were using patterns! A let statement looks like this, more formally:

let PATTERN = EXPRESSION;

We’ve seen statements like let x = 5; with a variable name in the PATTERN slot; a variable name is just a particularly humble form of pattern.

With let, we compare the expression against the pattern, and assign any names we find. So for example, in our let x = 5; case, x is a pattern that says “bind what matches here to the variable x.” And since the name x is the whole pattern, this pattern effectively means “bind everything to the variable x, whatever the value is.”

To see the pattern matching aspect of let a bit more clearly, consider Listing 18-4 where we’re using a pattern with let to destructuring a tuple:

let (x, y, z) = (1, 2, 3);

Listing 18-4: Using a pattern to destructure a tuple and create 3 variables at once

Here, we have a tuple that we’re matching against a pattern. Rust will compare the value (1, 2, 3) to the pattern (x, y, z) and see that the value matches the pattern. In this case, it will bind 1 to x, 2 to y, and 3 to z. You can think of this tuple pattern as nesting three individual variable patterns inside of it.

We saw another example of destructuring a tuple in Chapter 16, Listing 16-6, where we destructured the return value of mpsc::channel() into the tx (transmitter) and rx (receiver) parts.

Function Parameters

Similarly to let, function parameters can also be patterns. The code in Listing 18-5 declaring a function named foo that takes one parameter named x of type i32 should look familiar:

fn foo(x: i32) {
    // code goes here
}

Listing 18-5: A function signature uses patterns in the parameters

The x part is a pattern! In a similar way as we did with let, we could match a tuple in a function’s arguments. Listing 18-6 shows how we could split apart the values in a tuple as part of passing the tuple to a function:

Filename: src/main.rs

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({}, {})", x, y);
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

Listing 18-6: A function with parameters that destructure a tuple

This will print Current location: (3, 5). When we pass the value &(3, 5) to print_coordinates, the values match the pattern &(x, y). x gets the value 3, and y gets the value 5.

Because closures are similar to functions, as we discussed in Chapter 13, we can use patterns in closure parameter lists as well.

One difference between the places we can use patterns is that with for loops, let, and in function parameters, the patterns must be irrefutable. Let’s discuss that next.

results matching ""

    No results matching ""