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");
}
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"),
}
}
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}");
}
If you run this code, you will get:
Batman: Rich -- is batman true
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
totrue
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.
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."),
};
}
If you run this code, you will get:
Wonder Woman: LassoOfTruth
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:?}")
}
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."),
};
}
If you run this code, you will get:
Doesn't usually use weapons.
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 π.
Top comments (0)