Rust avoids the billion dollar mistake of including nulls 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.

  1. 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.