Learning Rust šŸ¦€: 13 - Pattern matching basics

fadygrab

Fady GA šŸ˜Ž

Posted on September 8, 2023

Learning Rust šŸ¦€: 13 - Pattern matching basics

We will continue our Enum mini-series. This time we will discuss the pattern matching basics using Enums. Let's begin.

āš ļø Remember!

You can find all the code snippets for this series in its accompanying repo

If you don't want to install Rust locally, you can play with all the code of this series in the official Rust Playground that can be found on its official page.

āš ļøāš ļø The articles in this series are loosely following the contents of "The Rust Programming Language, 2nd Edition" by Steve Klabnik and Carol Nichols in a way that reflects my understanding from a Python developer's perspective.

ā­ I try to publish a new article every week (maybe more if the Rust gods šŸ™Œ are generous šŸ˜) so stay tuned šŸ˜‰. I'll be posting "new articles updates" on my LinkedIn and Twitter.

Table of Contents:

The match control flow:

Another feature that I couldn't find in Python is the Pattern Matching. It's that control flow construct that is based on ... you've guessed it, patterns šŸ˜! Let's inspect that with an example:

fn main(){
    let die_roll = 4;

    if die_roll == 1 {
        println!("one");
    else if die_roll == 2 {
        println!("two");
    else if die_roll == 3 {
        println!("three");
    else if die_roll == 4 {
        println!("four");
    else if die_roll == 5 {
        println!("five");
    else {
        println!("six");
}
Enter fullscreen mode Exit fullscreen mode

In this example, we are throwing a die and print the number that we got in letters. Here, we are controlling the program flow based on the value of die_roll and the if expression should evaluate to either true or false. This is as far as we can go with if. We can re-write the previous example using match as follows:

fn main(){
    let die_roll = 4;

    match die_roll {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        4 => println!("four"),
        5 => println!("five"),
        6 => println!("six"),
    }
}
Enter fullscreen mode Exit fullscreen mode

This code snippet will work exactly the same as the previous one but with less typing šŸ˜‰. But match doesn't only provide less typing benefits over if, it can match based on types either and not only values. Take a look at the following example:

#[derive(Debug)]
enum Superhero {
    Batman,
    Superman,
    WonderWoman,
    Flash,
    GreenLantern,
    CatWoman,
}

fn main() {
    // Basic matching.
    let hero = Superhero::Batman;
    let mut is_batman = false;
    let super_power = match hero {
        Superhero::Batman => {
            is_batman = true;
            "Rich"
        }
        Superhero::Superman => "Flying, Bulletproof, Heat vision, Cold breath",
        Superhero::WonderWoman => "Flying, Strength, Divine weapons",
        Superhero::CatWoman => "Stealth, Agility",
        Superhero::Flash => "Speed, Connection with the speed-force",
        Superhero::GreenLantern => "Flying, Instant light constructs, Lantern Corps",
    };
    println!("{hero:?}: {super_power} -- is batman {is_batman}");
}

Enter fullscreen mode Exit fullscreen mode

If you run this code, you will get:

Batman: Rich -- is batman true
Enter fullscreen mode Exit fullscreen mode

Let's break it down. As you can see, we've created our Superhero Enum containing my favorite superheroes šŸ˜.

Note the #[derive(Debug)], remember this one šŸ˜‰?

Then, we have created hero variable that holds the Superhero variant BATMAAAAN šŸ¦‡ and an is_batman boolean flag that is initially set to false. Then, we've created super_power variable that gets its value form a match expression depending on the passed Superhero variant in hero. And Finally, we print what we get.

Now let's inspect the match block. It is constructed by using the match keyword then the "Scrutinee" (as described by the docs) or in simpler terms, the variable that we want to match against. Following that in the curly brackets, we have the "match arm" which is the value - or more generally the pattern - we are matching followed by the => operation then the "expression" we want to execute (or return). Usually, you don't want to use curly bracket in the "expression" block if the expression is short but if your expression block contains more than one expression/statement then their use is mandatory.

Check out the "BATMAAAN" expression where we have changed the value of is_batman to true in addition to returning its superpower as &str (string literal).

There are two things to consider when using match for pattern matching:
1. The match patterns must be exhaustive i.e. using all the variants of the checked variable.
2. The match arms must return the same type i.e. one match arm can't return i32 and another one returns &str.

Notice that in this example we have listed all the superheroes in our Superhero Enum and all the match arms are returning string literals.

Think of the match flow as a "Coins sorting machine" that executes the first match arm that matches the tested variable.

Coin sorting machine

Matching with patterns bind to values:

In the last article, we saw that we can bind data to Enums and using match is a way to use this bound data. Have a look at this example:

#[derive(Debug)]
enum SuperheroWithWeapon {
    Batman(FavoriteWeapon),
    Superman,
    WonderWoman(FavoriteWeapon),
    Flash,
    GreenLantern(FavoriteWeapon),
    CatWoman,
}

#[derive(Debug)]
enum FavoriteWeapon {
    LassoOfTruth,
    GreenLanternRing,
    Batarang,
}

fn main() {
    // Matching with patterns that bind to values with a catch-all.
    let hero_with_weapon_1 = SuperheroWithWeapon::WonderWoman(FavoriteWeapon::LassoOfTruth);
    match hero_with_weapon_1 {
        SuperheroWithWeapon::Batman(weapon) => {
            println!("Batman: {weapon:?}")
        }
        SuperheroWithWeapon::WonderWoman(weapon) => {
            println!("Wonder Woman: {weapon:?}")
        }
        SuperheroWithWeapon::GreenLantern(weapon) => {
            println!("Green Lantern: {weapon:?}")
        }
        other => println!("{other:?} doesn't usually use weapons."),
    };
}
Enter fullscreen mode Exit fullscreen mode

If you run this code, you will get:

Wonder Woman: LassoOfTruth
Enter fullscreen mode Exit fullscreen mode

Here, we've created two Enums, the SuperheroWithWeapon that accepts Superheroes with favorite weapons or without, and the FavoriteWeapon Enums that is passed to the previous one if the superhero uses a favorite weapon.

Notice the #[derive(Debug)] again.

This time, we've created a hero_with_weapon_1 variable that hold a WonderWoman variant of the SuperheroWithWeapon Enum and she uses the LassoOfTruth as her favorite weapon šŸŖ¢. In order to use the data of the favorite weapon that is bound to Wonder Woman, we have "named" it weapon in the match arm when we were defining the pattern to match against then we can use it normally in the match arm expression as in

SuperheroWithWeapon::WonderWoman(weapon) => {
            println!("Wonder Woman: {weapon:?}")
        }
Enter fullscreen mode Exit fullscreen mode

You have noticed that we didn't list all the Enum's variants this time and we only listed Batman, WonderWoman, GreenLantern, and a mysterious other placeholder?? Actually, this placeholder serves as a "catch all" match arm (or the else in the if expression). Therefore, our patterns in this example fulfil the match requirement of being exhaustive. Also, notice that all the match arms return the same unit tuple "()" (println! return value).

Using the "_" placeholder:

Another way to use a "catch all" match arm, is using the "_" place holder. But this time, we won't use any data in the match arm (similar to Python's "_" in for loops or tuples expansion).

#[derive(Debug)]
enum SuperheroWithWeapon {
    Batman(FavoriteWeapon),
    Superman,
    WonderWoman(FavoriteWeapon),
    Flash,
    GreenLantern(FavoriteWeapon),
    CatWoman,
}

#[derive(Debug)]
enum FavoriteWeapon {
    LassoOfTruth,
    GreenLanternRing,
    Batarang,
}

fn main() {
    // Matching with patterns that bind to values with _ placeholder.
    let hero_with_weapon_2 = SuperheroWithWeapon::Superman;
    match hero_with_weapon_2 {
        SuperheroWithWeapon::Batman(weapon) => {
            println!("Batman: {weapon:?}")
        }
        SuperheroWithWeapon::WonderWoman(weapon) => {
            println!("Wonder Woman: {weapon:?}")
        }
        SuperheroWithWeapon::GreenLantern(weapon) => {
            println!("Green Lantern: {weapon:?}")
        }
        _ => println!("Doesn't usually use weapons."),
    };
}
Enter fullscreen mode Exit fullscreen mode

If you run this code, you will get:

Doesn't usually use weapons.
Enter fullscreen mode Exit fullscreen mode

This time we have created hero_with_weapon_2 as a Superman variant which doesn't use any weapons. The match in this case will go to the "catch all" arm represented by the "_" placeholder which prints a generic message and doesn't use any data.

We will revisit "pattern matching" again later. In the next one, we will check an important Enum, the "Option" Enum which protects us from Null values bugs šŸ«¢. See you then šŸ‘‹.

šŸ’– šŸ’Ŗ šŸ™… šŸš©
fadygrab
Fady GA šŸ˜Ž

Posted on September 8, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related