Optional Values in Rust

By | January 20, 2022

Let’s say we are writing some code to retrieve data from a database. If the data we are looking for is there, we can obviously return it. If there is no data that matches our search, we need to signal that we did not find it. In many languages, we use a nil, null, or undef value as a flag for this missing value. This null reference was invented in 1965 by Sir Tony Hoare when designing the type system for the ALGOL W object oriented language. He has since referred to it as his “billion dollar mistake”.

Rust does not support a null reference, instead it has the Option<T> enum. Option has two variants: Some which contains the value in question and None which means there is no value. If you aren’t familiar with with Rust’s enums you might assume that None is just a different name for null. But, the reality is quite different.

An Optional Return

In the example that starts this post, let’s assume we have a User method that looks up a user by some userid. (We’ll ignore the actual implementation in this case.)

impl User {
   fn by_id(id: &str) -> Option<User> {
     ...
   }
}

Assuming we are supplied an id as a string reference, we look up the user. If we find a user with that id, we return a User object. The value we return is an Option<User>. Unlike the null reference case in other languages, we cannot use this value as if it were a User, we must extract the value first, which will fail if no User was found. One obvious way to extract the value is to use Rust’s pattern matching.

match User::by_id("gwadej") {
   Some(user) => {
      // do work with the user we found
   },
   None => {
      // report an error or otherwise handle the missing user case
   },
};

Obviously, you could do exactly the same thing in another language. But, in Rust, you cannot forget to “check for the missing user case”. You could obviously put the result of the User::by_id() method in a variable and pass it around, but eventually, in order to work with the User, you will have to resolve whether you actually got a value or not. This solves the problem that all of us see at one point or another where we try to access a value returned elsewhere in the code, find it’s a null and have to backtrack all through the code to find where the null came from.

More importantly, since null is not a possible value for an object, the only time you have to protect against a missing value is if you receive an Option<T>. If a variable or a parameter to a method has an actual type, then it must be a valid value of that type.

Option<T> Methods

Much like the Result<T, E> enum from the last post, using pattern matching to deal with an Option<T> is effective, but verbose. So, Rust provides a number of methods that are useful for common ways of handling missing values.

let v = opt_int.unwrap_or(42);  // the value of opt_int or 42 if None
let v = opt_int.unwrap_or_default(); // the value of opt_int or the default value for the type
let v = opt_int.unwrap_or_else(good_value); // the value of opt_int or result of executing good_value()

These methods deal with most of the ways you might use a supplied value or a default if not supplied.

Option supports many other methods, including map() which converts the actual value ignoring a None. There are also methods for chaining the generation of multiple Options, allowing you to attempt to retrieve a value from multiple methods, returning the first result that is not None.

Also like the Result, the question mark operator (?) can be applied to an Option<T> if the calling function returns an Option<T>. The ? would return the value if any or return None if there isn’t one.

Result and Option

In some ways, Result<T, E> and Option<T> are very similar. Both have a success variant and a failed variant. Depending on context, you could see very similar logic used to generate either enum. Because of that similarity, each type has methods for converting to the other.

// creates a result that successfully returns the value or returns a error if opt was None
let result = opt.ok_or(error_val);

// like above, but executes make_error to get the error value if None
let result = opt.ok_or_else(make_error);

// discard the error and return the value as an Option<T>.
let opt = result.ok();

// discard the value and return the error as an Option<E>.
let opt = result.err();

Conclusion

Although the Option<T> type is not unique to Rust, it is a very useful way to avoid the need for a null reference. The methods supplied by Option make it very flexible and convenient. Importantly, the methods on Option and Result streamline some very common use cases including conversion between the two types.

See the documentation for more details on how Options are used.

Leave a Reply

Your email address will not be published. Required fields are marked *