This blog covers concepts related to the following Rust topics:
Enum
- Pattern matching
the code is available on GitHub
enum
An enum
is similar to struct
in the sense that it is a type for which you can have multiple variants (same as that of a class and it's an instance). But the variants of an enum
are fixed and defined within the enum
itself.
A Player
might be represented by a struct
:
struct Player {
name: String,
rank: i32,
}
You can instantiate multiple instances of a Player
. But lets look at a type which is a little more niche
enum PlayerAccountType {
Free,
Paid,
}
Imagine you have only two account types - a free and a paid one. You can use an enum
to represent this concept. And, since it just another type, you can use it in the Player
struct
struct Player {
name: String,
rank: i32,
acc_type: PlayerAccountType,
}
you can represent
PlayerAccountType
type with astruct
, but don't need to since its possible variants are already known
You can reference enum variants using ::
. For e.g. to instantiate a Player
who has a paid account:
let paid_member = Player{acc_type: PlayerAccountType::Paid, name: String::from("john"), email: String::from("john@doe.com")};
It's also possible to add data to enum
variants. For e.g.
enum foo {
foo1(String),
foo2(String),
}
Note:
- In addition to data,
enum
s can also have methodsOption<T>
andResult<T,E>
are widely usedenum
s and are part of the Rust standard library
Pattern matching
Rust provides the match
keyword which behaves the same way as a switch
statement would (Rust does not have switch
). Before exploring match
, let's see a simple if-else-if
example:
let num = 101;
if num < 100 {
println!("{} is less than 100", num)
} else if num > 100 {
println!("{} is more than 100", num)
} else if num == 100 {
println!("{} is equal 100", num)
}
No surprises here! But you cannot write this using match
match num {
num < 100 => {
println!("{} is less than 100", num);
}
num < 100 => {
println!("{} is more than 100", num);
}
num == 100 => {
println!("{} is equal to 100", num);
}
}
This is not possible because the match
operator needs a value against which it can execute the match process. It does not execute a given expression for you.
Update: looks like
Guards
can help with the aboveif-else-if
scenario. Thanks to Richard for pointing this out!
The general format is as follows:
- you have a value you want to match against and a bunch of possible options, also known as a
match arm
- each arm is a combination of the pattern to match on and it's corresponding
expression
to be run if the match is successful
All this sounds abstract, so let's look at an example of match
ing with an enum
Match enum
Let's start by defining an enum
:
enum Choice {
One,
Two,
Three,
}
.. and match against Choice
s
//suppose this was passed by the user and you stored in a variable
let choice = Choice::One;
match choice {
Choice::One => println!("Option 1"),
Choice::Two => println!("Option 2"),
Choice::Three => println!("Option 3"),
}
Here, the variable choice
is the value being match
ed followed by three match
arms. For each arm, there is a pattern e.g. Choice::One
and the corresponding expression e.g. println!("Option 1")
separated by a =>
In this case, the output will be
Option 1
Match an Option
Rust standard library provides the Option
enum
whose purpose is to define a type to represent a scenario where you may or may not have a value. This makes the code obvious and is a better choice than using null
, nil
or similar options to denote the absence of a value
This is similar to
Optional
inJava
pub enum Option<T> {
Some(T),
None,
}
Don't worry about the
T
symbol. Just understand that it is ageneric
type parameter to allow theenum
to work with various types rather than tying it to a specific one e.g. aString
Let's understand this with an example. This is a simple CLI program that accepts an argument from the user (the name) and uses it to display a greeting. If the user does not pass anything, it uses a default greeting. This is a reasonable example of how to use Option
since we may or may not have an argument passed in by the uses (it's optional! duh!)
Here is ther corresponding function:
fn parse_name_arg() -> Option<String> {
let args: Vec<String> = std::env::args().collect();
if args.len() == 1 {
None
} else {
let name = &args[1];
Some(String::from(name))
}
}
parse_name_arg
function converts passed in arguments into a vector (Vec<String>
) and returns an Option
. If an argument was not passed, it returns None
or Some
(with the value) - both are variants of an Option
. Now we can use this function as such:
let name = parse_name_arg();
match name {
Some(n) => println!("Hello {}!", n),
None => println!("Hello there!"),
}
We store the result of parse_name_arg
in a variable called name
(which is an Option
), match different possibilities and execute the greeting accordingly. If you were to pass john
as an argument, you will get back Hello john!
. If you dont pass an argument, you will get back a generic greeting Hello there!
To try this out, just clone the GitHub repo, change to the correct directory i.e.
cd learning-rust/enums-match
and usecargo run
. To pass an argument, you can usecargo run <your argument
The value of an enum is available in the match
clause (also called arm
). This is why we were able to extract the passed in an argument using Some(n) => println!("Hello {}!", n),
Using let with match
How about storing the greeting in a variable? You can use let
with match
to return a value rather as well. Let's tweak the program a little bit
let greeting = match name {
Some(n) => n,
None => String::from("there"),
};
println!("Hello {}!", greeting)
We stored the result of the match in a variable called greeting and used with the println!
macro which makes the program much simpler!
Exhaust your choices!
By default, match
requires you to fulfill or account for all the possible options. Let's look at an example. Here is yet another enum
whose value we will match against
enum Days {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
and this is how we want to match (in this case, we happen to be interested in Friday
only)
let today = Days::Friday;
match today {
Days::Friday => println!("thank god its Friday!"),
}
But this will not compile. You will see an error as such:
error[E0004]: non-exhaustive patterns: `Monday`, `Tuesday`, `Wednesday` and 3 more not covered
To get around this problem, we can use the _
placeholder:
match today {
Days::Friday => println!("thank god its Friday!"),
_ => (),
}
This simply ignores the other possibilities. In such situations, you can also use if let
to keep it concise
let today = Days::Monday;
if let today = Days::Monday {
println!("its Monday already! :(");
}
Don't forget to check out these resources:
- The Rust Programming Language book
- Rust standard library documentation
- Rust by Example
That's all for a quick tour of enum
s and how you can use pattern matching with them. Stay tuned for more!