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:
Let’s create full names with/without a middle name:
Suppose we want to print the middle name if it is present. Let’s start with the simplest method, unwrap()
:
It works! Let’s try it with Jon:
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.
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.
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()
:
map
map()
is used to transform Option
values. For example, we could use map()
to print only the middle initial:
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:
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:
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):
Now, to figure out a person’s middle name’s nickname (slightly nonsensical, but bear with me here), we could do:
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. ↩