DEV Community

loading...

Learning Rust - Understanding pattern matching

brunooliveira profile image Bruno Oliveira ・4 min read

Introduction

Rust favors immutability by default, which can lead to somewhat unexpected behavior when coming from other languages.
We will see how pattern matching can be combined with immutability, and why it can be a powerful tool to have in our toolbox when dealing with code that we want to perform specific actions when encountering certain inputs.

Recap of let

Previously, we saw that let could be used to declare variables in Rust, and, when followed by the mut keyword, it would declare a mutable variable, whose value can be re-assigned during runtime, as opposed to an immutable variable which can't change. The syntax is as follows:

let x = 4;
let mut y = 1;

So, in the above snipped, we declare and initialize two variables that hold integer values, x and y.

The main difference between the two, is that y is mutable, whereas x is not. As a consequence, if we try to increment y, it all works perfectly:

use std::env;

fn main() {
    let x = 4;
    let mut y = 1;
    println!("Before update {}",y);
    y=y+1;
    println!("After update {}",y);
}

This will work as we want and output:

Before update 1
After update 2

However, when we try to do the same, but, for x which is immutable, here's what we will get:

error[E0384]: cannot assign twice to immutable variable `x`
 --> pattern_match.rs:7:5
  |
4 |     let x = 4;
  |         -
  |         |
  |         first assignment to `x`
  |         help: make this binding mutable: `mut x`
...
7 |     x=x+1;
  |     ^^^^^ cannot assign twice to immutable variable

The first line, describing the error is really clear: error[E0384]: cannot assign twice to immutable variable x: what this means is that we are attempting to re-assign a value to an immutable variable, which is not allowed.

So, we see that the way to use let, in case of both mutable and immutable bindings, is the same, but, the mutability or absence of it, can affect what we can do with the bindings during run-time.

Pattern matching

We can do more than simple value assignment with let, and, when dealing with a complex expression, we can use a pattern against which to match the expression to execute a certain action. Quoting the Rust book:

Patterns are a special syntax in Rust for matching against the structure of types, both complex and simple. Using patterns in conjunction with match expressions and other constructs gives you more control over a program’s control flow. A pattern consists of some combination of the following:

  • Literals
  • Destructured arrays, enums, structs, or tuples
  • Variables
  • Wildcards
  • Placeholders These components describe the shape of the data we’re working with, which we then match against values to determine whether our program has the correct data to continue running a particular piece of code.

To use a pattern, we compare it to some value. If the pattern matches the value, we use the value parts in our code.

We still do not know what all of these things are, but, we can already use what we know to introduce the syntax and idea of pattern matching.

The idea of pattern matching is as follows:

A pattern consists on a combination of components that describe the data we are working with, and, we can compare it against some value, and, when the pattern matches a certain value, we execute some action. For values that do not match the pattern, no action will be executed

Let's assume we want to print an informative message, that, if a certain value is 10, it means that a tram will arrive on line 10, and, if it is different, we assume that the tram will not be on line 10.

let tram_schedule = 10; //Tram is on line 10!!
match tram_schedule {
    10 => println!("Tram arriving to line 10"),
    _ => println!("No tram on line 10"),
}

If we run the above code, we will see the message Tram arriving to line 10

Let's go over the syntax:

We have the definition of a match expression above. From the Rust docs:

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, like this:

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

One requirement for match expressions is that they need to be exhaustive in the sense that all possibilities for the value in the match expression must be accounted for. One way to ensure you’ve covered every possibility is to have a catchall pattern for the last arm: for example, a variable name matching any value can never fail and thus covers every remaining case.

A particular pattern _ will match anything, but it never binds to a variable, so it’s often used in the last match arm. The _ pattern can be useful when you want to ignore any value not specified, for example.

We can also match against a range of values. Let's assume now that we are waiting for a tram, and, if the value of our waiting time is between 1 and 10, we assume to be on time, if else, it is delayed.

Here's how we can match against an inclusive range of values:

let time = 10;

match time {
    1..=10 => println!("Tram on time"),
    _ => println!("Delayed"),
}

The special syntax, ..= can be used to match a pattern against a range of values.

We can also use the pipe operator | serving as an or, matching one value or the other, like in:

let x = 1;

match x {
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    _ => println!("anything"),
}

The above code will print one or two.

Conclusion

This was a short introduction to pattern matching and how it is useful to control execution flow of our Rust programs.
We saw how it can be used to handle more complex cases like matching sets or even ranges of values, and how it can be more versatile than simpler if-else constructs.

The mandatory pattern that needs to be always present to cover any non-matched pattern makes the code more robust and safer as well!

Discussion (6)

Collapse
deciduously profile image
Ben Lovy • Edited

Awesome write-up - i don't think it ever occurred to me to match on a range like that. Pattern matching is one of my favorite features in any language that offers it. I find myself using match extensively, almost excessively, and it pairs really well with enums for this reason.

The mandatory pattern that needs to be always present

If you use an enum, you must exhaustively match each variant, and if you avoid using _ as a catch-all when matching on an enum type, you can now fearlessly add functionality and just trust the compiler to walk you through each part of your code that needs to change! You can de-structure the variant right in the match arm, too:

match some_option {
    Some(value) => println!("{}", value),
    None => println!("Option contained no value!"),
}

My Rust projects tend to end up with a bunch of enum types that carry inner values and structs, not a pattern I'd been used to in any other language. Because of this, refactoring in Rust is fearless and super pleasant.

Collapse
brunooliveira profile image
Bruno Oliveira Author

Awesome stuff! I can't wait to get even more familiar with the language and its ecosystem and frameworks to start a larger project!

Collapse
deciduously profile image
Ben Lovy

Looking forward to reading about it !

Collapse
rustlangtr profile image
rust-lang-tr

Hello Bruno,
We have translated your article into Turkish and request your permission to publish it on Rustdili.com.
Example: rustdili.com/oruntu-esleme/
Good work

Collapse
brunooliveira profile image
Bruno Oliveira Author

Hello :) please, by all means publish it and use it! Happy to see it getting noticed

Collapse
rustlangtr profile image
rust-lang-tr

Thank you so much:-)

Forem Open with the Forem app