Rust Options in Practice
Rust doesn't have a nil
value commonly found in other languages. This prevents annoying run-time
errors like dereferencing a nil
pointer, but the need to express a "nothing" value remains. Rust
provides an Option<T>
type to represent these optional values. They can either be something or
nothing. These two possible states correspond to Some(T)
and None
. There's plenty of good
introductory and high-level documentation on how to use Option<T>
available in the standard
places. This article presents a few practical examples of how to use Option<T>
that you may
encounter.
Do Something with it if it's Something
We often have an option, and we want to perform some additional work on the value of that option.
But if the option is None
the work doesn't need to be done and it can simply remain None
.
map
is a method of Option<T>
that facilitates this. It has some slight similarities with map
on an iterator in that it allows for modifying what's in the option, but other than that its best to
think of it as a completely different thing. If the original option is Some
, map
always
returns Some
, wrapping whatever is returned from the callback function with Some
. The callback
can return a different type than the original option, allowing for changing from an Option<T>
to
an Option<U>
.
let maybe = Some(1);
let changed = maybe.map(|val| val * 2).map(|val| val * 2);
// changed is Some(4) because 1 * 2 * 2 = 4
println!("{}", changed.unwrap()); // 4
let maybe = Some(1);
let changed: Option<&str> = maybe.map(|_| 1).map(|_| "a");
println!("{}", changed.unwrap()); // "a"
and_then
is similar in some ways to map
. and_then
doesn't always return Some
: The callback
function itself returns an option. If the returned value is None
the chain stops and results in
None
. and_then
is useful for processing an option with operations that can themselves produce
options.
let maybe = Some(1);
let changed: Option<&str> = maybe
.and_then(|_| Some("hello"))
.and_then(|_| None::<Option<&str>>)
.and_then(|_| Some("goodbye")); // changed is None
Borrow it if it's Something
Borrowing the value contained in an option is useful if something should be done with the value
(unless it's None
) without taking ownership of it, leaving the value available for later use.
First consider the alternative: Taking ownership of the value in the option, which makes it unable to be used later:
let maybe = Some("hello".to_string());
if let Some(maybe) = maybe {
println!("{}", maybe);
}
println!("{}", maybe.unwrap()); // Compiler error! Use of partially moved value.
as_ref
provides a way to get a reference to the value (aka: a borrow) instead:
let maybe = Some("hello".to_string());
if let Some(maybe) = maybe.as_ref() {
// Do something with the borrowed Some value, without taking it out.
println!("{}", maybe);
}
println!("{}", maybe.unwrap()); // The original value in the option is still there.
Using the ref
keyword is equivalent:
let maybe = Some("hello".to_string());
if let Some(ref maybe) = maybe {
// Do something with the borrowed Some value, without taking it out.
println!("{}", maybe);
}
println!("{}", maybe.unwrap()); // The original value in the option is still there.
For a mutable borrow, use as_mut
or ref mut
:
let mut maybe = Some(1);
if let Some(my_mut) = maybe.as_mut() {
*my_mut = 2;
}
println!("{}", maybe.unwrap()) // 2
let mut maybe = Some(1);
if let Some(ref mut my_mut) = maybe {
*my_mut = 2;
}
println!("{}", maybe.unwrap()) // 2
Borrow and Dereference if it's Something
It can be handy to borrow and dereference the value contained in an option depending on how it needs
to be used. This may apply when dealing with options and smart pointers. Depending on what you need
out of the option, as_deref
(and as_deref_mut
) can be a nice convenience:
fn main() {
let maybe = Some(Box::new(42));
first(maybe.as_deref());
second(maybe.as_ref());
println!("{}", maybe.unwrap()); // 42
}
fn first(s: Option<&i32>) {
println!("{}", s.unwrap()) // 42
}
fn second(s: Option<&Box<i32>>) {
println!("{}", s.unwrap()) // 42
}
as_deref
isn't doing anything magical. The same thing can be done with the dereference operator,
but it starts to get ugly:
fn main() {
// ...
first(maybe.as_ref().map(|v| &**v));
// ...
}
Re-using Options
Rather than borrowing the value from an option so that it can be used again, sometimes we want to
take ownership of the value but leave the option conceptually in place so that it can be used again
as an option. The option can't possibly be Some(original_thing)
anymore after the value is taken
over, so it needs to be replaced by something else.
An example is a structure containing work to be done. If the program takes work from it, there is no
work left and that is represented by None
. If there's work to be done it's Some(work)
. The
structure can be continuously re-used in this way to take and hold work.
As a first pass, we might try to do something like this:
struct Thing {
work: Option<String>,
}
impl Thing {
fn do_work(&mut self) -> Option<String> {
self.work.map(|s| { // Compiler error! Can't move out of self.work
self.work = None;
s
})
}
fn create_work(&mut self, work: String) -> Result<(), &'static str> {
match self.work {
Some(_) => Err("cannot create work when there is already work to do!"),
None => {
self.work = Some(work);
Ok(())
}
}
}
}
The error in this initial implementation is because we can't move the value out and then put a new
value back in, even if putting the new value back in happens right away. This would leave
self.work
in an invalid state, if only very briefly, which is a violation of exception
safety. To take a value and
replace it in a single operation, use std::mem::replace
or (more commonly) the more convenient
take
method:
fn do_work(&mut self) -> Option<String> {
self.work.take()
}
// ... equivalent to ...
fn do_work(&mut self) -> Option<String> {
std::mem::replace(&mut self.work, None)
}
There's also a replace
method that can be used to replacing the value with something other than
None
.
Interchanging Options and Results
Functions frequently need to return Result<T>
but end up dealing with Option<T>
or the other way
around.
ok_or
and ok_or_else
can be used to return an error if an option is None.
This works nicely
with the ?
operator for handling these cases where encountering a None
should be treated as a
run-time error. ok_or_else
takes a callback function which allows for lazy-evaluation of the code
it contains: Use this to avoid unnecessary processing or unwanted side effects unless the option
truly is None
:
fn fallible() -> anyhow::Result<()> {
let maybe = Some(2);
maybe.ok_or_else(|| anyhow::anyhow!("some error"))?;
Ok(())
}
An error result can be changed to an optional value using ok
. This applies to cases where a
potentially fallible operation is done and the outcome should not be treated specifically as an
error, but rather as None
:
fn infallible1() -> Option<i32> {
Err("something").ok() // Returns None
}
fn infallible2() -> Option<i32> {
Ok::<i32, anyhow::Error>(1).ok() // Returns Some(1)
}
transpose
is another useful method for more complicated cases. On its own it makes an option of a
result into a result of an option. Combined with map
and the ?
operator this can express
otherwise cumbersome operations in a succinct way. A pattern where this applies is shown below where an optional value is subject to some fallible processing:
- If the input is
None
, the output isOk(None)
. - If the input is
Some(val)
andval
cannot be processed, the output reflects the processing error. - If the Input is
Some(val)
andval
can be processed, the output isOk(processed_value)
.
fn str_to_int(parse_this: Option<&str>) -> anyhow::Result<Option<i32>> {
Ok(parse_this.map(|str_val| str_val.parse()).transpose()?)
}