We will finish off our Enum and pattern matching discussion that was started here by a case study on the Option standard enum. 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:
Problem of Null:
How many times have you faced bugs in your code due to an "unexpected" null value? I'll answer that, plenty π
The fix is easy, in python for example you can write something like that:
if value is None:
print("A none value!")
None and Null are the same in Python
But you have to remember to do that for every value (or data structure) that may become - for some reason π€·ββοΈ - absent or invalid (i.e. Null) which might not be very scalable in a large code base.
Rust fixes that! by ... removing the Null π! Yes, you have read it write, there is no intrinsic Null type in Rust.
But ...
There is a standard Enum called "Option" (Option) that can be used in this use case.
The Option<T> Enum:
As I've mentioned, Rust has a standard Enum called "Option" which is loaded in the "prelude" (you don't have to load it with the use
keyword) to deal with the Null situation that we have here. It has two variants:
- Some(T)
- None
And it is defined like this:
enum Option<T> {
Some(T),
None,
}
The first thing to notice is that weird "<T>" notation. This is a generic type notation (T for Type) and we will encounter it later on. What's important now is that you imagine "Option" as a "wrapper" for Any Type that you use in your program that might be either "Some" value or "None" (Null) and as we have learned in the last article, that pattern matching for Enums must be "Exhaustive", when using the Option Enum the compiler will force the Null value handling sparing you from remembering to do that yourself.
Using the Option Enum:
The Option Enum is load into the Rust program prelude by default so as its variants Some(T) and None and you don't have to use namespacing to use them.
Something like
Option<i32>::Some(5)
isn't necessary. Just useSome(5)
.
fn main() {
// Using the Option Enum without importing it.
let some_number = Some(5);
let name = Some("My Name");
let none: Option<i32> = None;
}
Here, we are directly using Some
and None
. Note that for Some
, it can take any Type and the compiler can infer that type. But for None
, we have to set its type.
The Option/ Enum has a set of useful methods like is_some()
or is_none()
. See the docs for a complete listing:
fn main() {
// Using the Option Enum without importing it.
let some_number = Some(5);
let none: Option<i32> = None;
// Some Option Enum methods
assert_eq!(some_number.is_some(), true);
assert_eq!(none.is_none(), false); // Will cause assertion error
}
One important thing to know, is that Some(5)
isn't 5
! Let's try it out:
fn main() {
// Using the Option Enum without importing it.
let some_number = Some(5);
let number = 5;
let addition = some_number + number;
}
If we run this code, we will get the following compilation error:
error[E0369]: cannot add `{integer}` to `Option<{integer}>`
--> src/main.rs:13:32
|
13 | let addition = some_number + number;
| ----------- ^ ------ {integer}
| |
| Option<{integer}>
Basically, what this error message is telling us is that Rust don't know how to add a 5
that is wrapped in Option Enum with a normal i32
5 as they are now two different types.
So how could we do that??π€
This is when Pattern Matching comes in and the purpose of the Option Enum becomes clear.
We will demo the use of bound data by using a function that doubles what's inside Some
:
fn main() {
// Using the data bound to Some.
let five = Some(5);
let mut result = double(five);
println!("Double of 5 is: {result:?}");
result = double(none);
println!("Double of None is: {result:?}");
}
fn double(num: Option<i32>) -> Option<i32> {
match num {
Some(n) => Some(n * 2),
None => None,
}
}
The function is called double
and it takes an Option Enum with i32
data type and outputs the same type. Inside the function, there is a match
expression that has to matching arms for the two Option variants, Some and None. And hence pattern matching must be exhaustive, the compiler checks if all variants are mentioned and therefore enforces a "Null" handling arm.
If we run this code we will get:
Double of 5 is: Some(10)
Double of None is: None
So, when should we use the Option Enum you say?
We should use it whenever our values could be a Null because as long as the value isn't wrapped in it, Rust assumes that it will be always valid through the program execution.
The Option Enum is yet another way where Rust enforces its safety guard rails and we should use that for our advantage.
If Let: alternate way to pattern matching:
One more thing before we leave pattern matching (for now π), is the if let
expression. Image that you are receiving a sensor readings that may be null due to network problems, corrupt data, or for whatever reason and you are interested in printing the value when it's 5
only. One way to do it is as follows:
fn main(){
let reading: Option<u8> = Some(5);
match reading {
Some(5) => println!("Reading of 5 is received [match]"),
_ => (),
}
}
Remember the "_" placeholder from here? Here, we are only interested when we receive Some(5)
. Any other value including Null, will produce the Unit tuple () (basically, do nothing). But this is a bit verbose.
Another way to write the same code is by using if let
:
fn main(){
let reading: Option<u8> = Some(5);
if let Some(5) = reading {
println!("Reading of 5 is received [if let]")
}
}
If we run this code, you will get the same result as before but less verbose. The if let
syntax is a bit confusing but once you use it often, it will make sense:
if let <pattern> = <value> {
<expression(s) to be executed if matched>
} else {
<expression(s) to be executed if not matched>
}
So, when to use if let
? There is no advantage of using if let
over match
for "one" pattern matching except it's less verbose. So, using one over the other depends on what you are trying to do...
If you want to take advantage of the exhaustiveness of the match
, use it. Otherwise, if you are only interested in one match and you can safely ignore all the others, use if let
.
As show,
if let
has andelse
clause that that be used as a "catch-all" for all other not matched patterns
Next, we will explore more about how we can use Packages, Crates and Modules in Rust. I may break it into several articles too. See you then π.
Top comments (0)