DEV Community

Cover image for Learning Rust πŸ¦€: 14 - Option Enum: An Enum and Pattern matching use case
Fady GA 😎
Fady GA 😎

Posted on

Learning Rust πŸ¦€: 14 - Option Enum: An Enum and Pattern matching use case

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!")
Enter fullscreen mode Exit fullscreen mode

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:

  1. Some(T)
  2. None

And it is defined like this:

enum Option<T> {
    Some(T),
    None,
}
Enter fullscreen mode Exit fullscreen mode

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 use Some(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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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}>
Enter fullscreen mode Exit fullscreen mode

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,
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
safety first

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]"),
        _ => (),
    }
}
Enter fullscreen mode Exit fullscreen mode

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]")
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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 and else 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)