Option

In languages that have the notion of nullable values, there is a defensive code style that programmers adopt to perform operations on any value that can possibly be null. Taking an example from Kotlin/Java, it appears something like this:

// kotlin pseudocode

val container = collection.get("some_id")

if (container != null) {
container.process_item();
} else {
// no luck
}

First, we check that container is not null and then call process_item on it. If we forget the null safety check, we'll get the infamous NullPointerException when we try to invoke container.process_item()  you only get to know this at runtime when it throws the exception. Another downside is the fact that we can't deduce right away whether container is null just by looking at the code. To save against that, the code base needs to be sprinkled with these null checks, which hinder its readability to a great extent.

Rust does not have the notion of null values, which is infamously quoted as being the billion-dollar mistake by Tony Hoare, who introduced null references in the ALGOL W language back in 1965. In Rust, APIs that might fail and want to indicate a missing value are meant to return Option. This error type is suitable when any of our APIs, along with a succeeding value, want to signify the absence of a value. To put it simply, it's quite analogous to nullable values, but here, the null check is explicit and is enforced by the type system at compile time.

Option has the following type signature:

pub enum Option<T> {
    /// No value
    None,
    /// Some value `T`
    Some(T),
}

It's an enum with two variants and is generic over T. We create an Option value by using either let wrapped_i32 = Some(2); or let empty: Option<i32> = None;.

Operations that succeed can use the Some(T) variable to store any value, T, or use the None variable to signify that the value is null in the case of a failed state. Though we are less likely to create None values explicitly, when we need to create a None value, we need to specify the type on the left, as Rust is unable to infer the type from the right-hand side. We could have also initialized it on the right, as None::<i32>; using the turbofish operator, but specifying the type on the left is identified as idiomatic Rust code.

As you may have noticed, we didn't create the Option values through the full syntax, that is, Option::Some(2), but directly as Some(2). This is because both of its variants are automatically re-exported from the std crate (Rust's standard library crate) as part of the prelude module (https://doc.rust-lang.org/std/prelude/). The prelude module contains re-exports of most commonly used types, functions, and any modules from the standard library. These re-exports are just a convenience that's provided by the std crate. Without them, we would have to write the full syntax every time we needed to use these frequently used types. As a result, this allows us to instantiate Option values directly through the variants. This is also the case with the Result type.

So, creating them is easy, but what does it look like when you are interacting with an Option value? From the standard library, we have the get method on the HashMap type, which returns an Option:

// using_options.rs

use
std::collections::HashMap;

fn main() {
let mut map = HashMap::new();
map.insert("one", 1);
map.insert("two", 2);

let value = map.get("one");
let incremented_value = value + 1;
}

Here, we create a new HashMap map of &str as the key and i32 as the value, and later, we retrieve the value for the "one" key and assign it to the value . After compiling, we get the following error message:

Why can't we add 1 to our value? As someone familiar with imperative languages, we expect map.get() to return an i32 value if the key exists or a null otherwise. But here, value is an Option<&i32>. The get() method returns an Option<&T>, and not the inner value (a &i32) because there is also the possibility that we might not have the key we are looking for and so get can return None in that case. It gives a misleading error message, though, because Rust doesn't know how to add an i32 to a Option<&i32>, as no such implementation of the Add trait exists for these two types. However, it indeed exists for two i32's or two &i32's.

So, to add 1 to our value, we need to extract i32 from Option. Here, we can see Rust's explicit error handling behavior spring into action. We can only interact with the inner i32 value after we check whether map.get() is a Some variant or a None variant.

To check for the variants, we have two approaches; one of which is pattern matching or if let:

// using_options_match.rs

use std::collections::HashMap;

fn main() {
let mut map = HashMap::new();
map.insert("one", 1);
map.insert("two", 2);

let incremented_value = match map.get("one") {
Some(val) => val + 1,
None => 0
};

println!("{}", incremented_value);
}

With this approach, we match against the return value of map.get() and take actions based on the variant. In the case of None, we simply assign 0 to incremented_value. Another way we could have done this is by using if let:

let incremented_value = if let Some(v) = map.get("one") {
v + 1
} else {
0
};

This is recommended for cases where we are only interested in one variant of our value and want to do a common operation for other variants. In those cases, if let is much cleaner.

Unwrapping: The other, less safe, approach is to use unwrapping methods on Option, that is, the unwrap() and the expect() methods. Calling these methods will extract the inner value if it's a Some, but will panic if it's a None. These methods are recommended only when we are really sure that the Option value is indeed a Some value:

// using_options_unwrap.rs

use std::collections::HashMap;

fn main() {
let mut map = HashMap::new();
map.insert("one", 1);
map.insert("two", 2);
let incremented_value = map.get("three").unwrap() + 1;
println!("{}", incremented_value);
}

Running the preceding code panics, showing the following message because we unwrapped a None value as we don't have any value for the three key:

thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', libcore/option.rs:345:21
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Between the two, expect() is preferred because it allows you to pass a string as a message to be printed upon panic, and shows the exact line number in your source file where the panic happened, whereas unwrap() does not allow you to pass debug messages as arguments and shows a line number in the standard library source file where the unwrap() method of Option is defined, which is not very helpful. These methods are also present on the Result type.

Next, let's look at the Result type.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset