Rust: Using Options by example
Rust avoids the billion dollar mistake of including
null
s in the language. Instead, we can represent a value that might or might not exist with the Option
type.
This is similar to Java 8 Optional
or Haskell’s Maybe
. There is plenty
of material out there detailing why an Option type is better than null, so I won’t go too much into that.
In Rust, Option<T>
is an enum that can either be None
(no value present) or Some(x)
(some value present).
As a newbie, I like to learn through examples, so let’s dive into one.
Example
Consider a struct that represents a person’s full name. The first and last names are mandatory, whereas the middle name may or may not be present. We can represent such a struct like this 1:
struct FullName {
first: String,
middle: Option<String>,
last: String,
}
Let’s create full names with/without a middle name:
let alice = FullName {
first: String::from("Alice"),
middle: Some(String::from("Bob")), // Alice has a middle name
last: String::from("Smith")
};
let jon = FullName {
first: String::from("Jon"),
middle: None, // Jon has no middle name
last: String::from("Snow")
};
Suppose we want to print the middle name if it is present. Let’s start with the simplest method, unwrap()
:
println!("Alice's middle name is {}", alice.middle.unwrap()); // prints Bob
It works! Let’s try it with Jon:
println!("Jon's middle name is {}", jon.middle.unwrap()); // panics
So, unwrap()
panics and exits the program when the Option
is empty i.e None
. This is less than ideal.
Pattern matching
Since Option
is actually just an enum
, we can use pattern matching to print the middle name if it is present, or a default message if it is not.
println!("Jon's middle name is {}",
match jon.middle {
None => "No middle name!",
Some(x) => x,
}
);
This fails compilation with the error:
error[E0308]: match arms have incompatible types
--> src/main.rs:28:9
|
28 | / match jon.middle {
29 | | None => "No middle name!",
30 | | Some(x) => x,
31 | | }
| |_________^ expected &str, found struct `std::string::String`
Recall in my earlier post, that a string literal is actually
a string slice. So our None
arm is returning a string slice,
but our Some
arm is returning the owned String
struct member. Turns out we can conveniently use ref
in a pattern match
to borrow a reference. Again, recalling that &String
can be coerced to &str
, this solves our type mismatch problem.
println!("Jon's middle name is {}",
match jon.middle {
None => "No middle name!",
Some(ref x) => x, // x is now a string slice
}
);
This works!
Option methods
Pattern matching is nice, but Option
also provides several useful methods. We can achieve what we did in the previous section with unwrap_or()
:
println!("Alice's middle name is {}",
alice.middle.unwrap_or("No middle name!".to_owned()));
map
map()
is used to transform Option
values. For example, we could use map()
to print only the middle initial:
println!(
"Alice's full name is {} {} {}",
alice.first,
alice.middle.map(|m| &m[0..1]).unwrap_or(""), // Extract first letter of middle name if it exists
alice.last
);
However, this fails to compile with the very clear error:
42 | | alice.middle.map(|m| &m[0..1]).unwrap_or(""),
| | - ^ `m` dropped here while still borrowed
| | |
| | borrow occurs here
Ah, so map()
consumes the contained value, which means the value does not live past the scope of the map()
call!
Luckily, the as_ref()
method of Option
allows us to borrow a reference to the contained value:
println!(
"Alice's full name is {} {} {}",
alice.first,
alice.middle.as_ref().map(|m| &m[0..1]).unwrap_or(""), // as_ref() converts Option<String> to Option<&String>
alice.last
);
Instead of first using map()
to transform to another Option
and then unwrapping it, we can use the convenience
method map_or()
which allows us to do this in one call:
alice.middle.as_ref().map_or("", |m| &m[0..1])
and_then
and_then()
is another method that allows you to compose Options (equivalent to flatmap in other languages).
Suppose we have a function that returns a nickname for a real name, if it knows one. For example, here is such a
function (admittedly, one that has a very limited worldview):
fn get_alias(name: &str) -> Option<&str> {
match name {
"Bob" => Some("The Builder"),
"Elvis" => Some("The King"),
_ => None,
}
}
Now, to figure out a person’s middle name’s nickname (slightly nonsensical, but bear with me here), we could do:
let optional_nickname = alice.middle.as_ref().and_then(|m| get_nickname(&m));
println!("Alice's middle name's nickname is {}",
optional_nickname.unwrap_or("(none found)")); // prints "The Builder"
In essence, and_then()
takes a closure that returns another Option
. If the Option
on which and_then()
is called is present,
then the closure is called with the present value and the returned Option
becomes the final result. Otherwise, the final result
remains None
. As such, in the case of jon
, since the middle name is None
, the get_nickname()
function will not be called at all,
and the above will print “(none found)”.
Summary
Rust provides a robust way to deal with optional values. The Option
enum has several other useful methods I didn’t cover. You can
find the full reference here.
-
Experienced Rust programmers would probably have the struct members be string slices, but that would require use of lifetimes, which is outside the scope of this post. ↩