Unlike many languages, Optional type is a fundamental mechanism in Rust development - it's behind its null safe characteristic. But beyond working as an Optional pointer, the Option type has some very useful features. Option is an enum, in Rust this means we have the power of algebraic enums, which allows carrying a specific value. Option is an enum of varieties None and Some(T). Many of its operations have a functional format - which makes a callback through closure, being quite efficient as it's invoked only when necessary. Let's explore this and much more!
Getting the Value
To get the internal value, you can use the well-known forms if let and match. The main difference is that match is exhaustive, meaning we must handle both possibilities of the Option enum. Example:
let opt_some_1 = Some(1);
if let Some(i) = opt_some_1 {
println!("{}", i);
}
match opt_some_1 {
Some(i) => println!("{}", i),
None => println!("None"),
}
To simply know if the Option has content or not, we can use is_some and is_none:
let opt_some = Some(1);
assert_eq!(opt_some.is_some(), true);
let opt_none = None::<i32>;
assert_eq!(opt_none.is_none(), true);
We might want to get the value, but providing a default if it doesn't exist. For this, we use unwrap_or and unwrap_or_else, the latter being the functional format. Example:
let opt_none : Option<i32> = None;
let result = opt_none.unwrap_or(1);
let result = opt_none.unwrap_or_else(|| 1);
There's also get_or_insert_with which seems to work like unwrap_or_else, but it updates the object's content, so it needs to be mutable (mut). The great benefit of this method is that we can use it as a lazy load trick - which loads only once and only at the moment of use, if it's even going to be used. Example:
let opt_none : Option<i32> = None;
let result = opt_none.get_or_insert_with(|| 1);
We can also get the value through unwrap, but if Option has None it will cause a panic. So we need to be very careful when using it.
You Can Imagine Option as an Iterator!
Initially it may seem strange, but try to imagine Option as an iterator with size ranging from 0 to 1. This means we can do some operations identical to those we use with Vec, for example.
Using for:
let opt_some_1 = Some(1);
for i in opt_some_1 {
println!("for opt_some_1: {:?}", i);
}
It's also possible to do operations through functional format.
Let's suppose we want to keep the value, if it exists, given a condition. For this, we can use filter:
let opt_some_1 = Some(1);
let filtered_opt = opt_some_1.filter(|i| i % 2 == 0);
assert_eq!(filtered_opt, None);
To do a map operation, which generates a new Option with modified value, if it exists:
let opt_some_1 = Some(1);
let opt_map = opt_some_1.map(|i| i * 2);
assert_eq!(opt_map, Some(2));
It's also possible to map the operation and provide a value when it doesn't exist, through map_or. In this case, it always returns a value:
let opt_some_1 = Some(1);
let opt_map_or = opt_some_1.map_or(4, |i| i * 2);
assert_eq!(opt_map, Some(2));
Option can also return iter, into_iter and iter_mut:
let opt_some_1 = Some(1);
let found_1 = opt_some_1.iter().any(|i| i == &1);
assert_eq!(found_1, true);
Chaining Conditions
We can chain successive attempts to get the value with other Options, working like a logical circuit.
Here we'll consider OptionA the Option that's calling the method and OptionB the Option being passed as parameter.
The and method returns OptionB if both OptionA and OptionB are Some(T).
The or method returns OptionA (preferentially) or OptionB if one of them is Some(T).
Example:
let option_a = Some(1);
let option_b = Some(2);
let option_none: Option<i32> = None;
// and: returns OptionB if both are Some
assert_eq!(option_a.and(option_b), Some(2));
assert_eq!(option_a.and(option_none), None);
// or: returns OptionA preferentially, or OptionB
assert_eq!(option_a.or(option_b), Some(1));
assert_eq!(option_none.or(option_b), Some(2));
There are also the functional versions and_then and or_else:
let option_a = Some(1);
let result = option_a.and_then(|i| Some(i * 2));
assert_eq!(result, Some(2));
Converting Option to Result
Option can be converted to Result through ok_or and ok_or_else:
let opt_some = Some(1);
let result: Result<i32, &str> = opt_some.ok_or("error");
assert_eq!(result, Ok(1));
let opt_none: Option<i32> = None;
let result: Result<i32, &str> = opt_none.ok_or("error");
assert_eq!(result, Err("error"));
Conclusion
Option is one of Rust's most used types. Understanding its methods is essential to write clean and efficient code. The functional approach with map, filter, and_then, etc., allows chaining operations elegantly without the need for multiple if let or match checks.