Options and Results


Prerequisites:

  • You should be familiar with Rust
  • iterators
  • from the iterators chapter (also explained in async), make sure you read the existential types part

Not everything is perfect in this world, and not every operation has to always succeed and produce a value. If you are familiar with other programming languages, the concepts of nullability, or optionally present values, and fallibility, or in other words, that an operation might fail with an error, should not be foreign to you.

Maybe you have noticed that already, but neither can values in Rust be null, nor are there exceptions in Rust. These are both considered concepts that harm the type system by being hidden behavior, if you have the following function:

#![allow(unused)]
fn main() {
fn my_function() -> bool {
    unimplemented!()
}
}

Just from the signature, it is easy to recognize that it can either return true or false, but also having to have to count with the fact that it could return a null or throw an exception is non-trivial.

To leverage the power of the type system, Rust elected to forgo null and exceptions completely, and instead encode this information into types.

Now, if you want to indicate an operation might not return a value, you use the Option type:

#![allow(unused)]
fn main() {
fn my_function() -> Option<bool> {
    unimplemented!()
}
}

And if you want to indicate that an operation might fail, you use the Result type:

#![allow(unused)]
fn main() {
fn my_function() -> Result<bool, MyErrorType> {
    unimplemented!()
}
}

The great thing about Option and Result is that they are in no way special. They are merely enums.

#![allow(unused)]
fn main() {
enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E: Error> {
    Ok(T),
    Err(E),
}
}

This is roughly how the types look. The best part is that they are very effective. Result is a very thin wrapper and Option is optimized into being a nullable pointer to T.

Errors in Result

You may have noticed that Result takes two type parameters, the second one being E: Error. It is required that the second type in a Result implements std::error::Error, which is essentially a fancy display trait that can optionally allow pointing at another instance of an Error-implementing type as the root cause:

#![allow(unused)]
fn main() {
pub trait Error: Debug + Display {
    fn source(&self) -> Option<&(dyn Error + 'static)> { ... }
    fn backtrace(&self) -> Option<&Backtrace> { ... }
    fn description(&self) -> &str { ... }
    fn cause(&self) -> Option<&dyn Error> { ... }
}
}

As you can see, all of the trait methods have a default implementation, and, as a matter of fact, description() and cause() are further marked as deprecated, so, if your type implements Debug + Display, implementing the Error trait is usually as easy as:

#![allow(unused)]
fn main() {
impl Error for MyType {}
}

Traditionally, you do not type out the error type at every step, but create an aliased Result type for your library, a particular module, or another section of your code at your discretion:

#![allow(unused)]
fn main() {
use std::result;

type Result<T> = result::Result<T, MyError>;

fn my_function() -> Result<bool> {
    unimplemented!()
}
}

This pattern is used many times in the standard library:

https://doc.rust-lang.org/std/index.html?search=Result

If you need to have two or more different aliased Result types in scope, you can always use a renaming import:

#![allow(unused)]
fn main() {
use std::io::Result as IoResult;
use std::thread::Result as ThreadResult
use std::result;

type Result<T> = result::Result<T, MyError>;

fn my_function() -> Result<bool> {
    unimplemented!()
}
}

Options to results, Results to options

Often, you need to convert one to the other. This is quite easy in Rust, especially if you need to convert from Result to Option.

There are two possible conversions:

  • Result<T, E> -> Option<T> to extract the contained value, use .ok()
  • Result<T, E> -> Option<E> to extract the contained error, use .err()

https://doc.rust-lang.org/std/result/enum.Result.html#method.ok

https://doc.rust-lang.org/std/result/enum.Result.html#method.err

#![allow(unused)]
fn main() {
let x: Result<u32, &str> = Ok(2);
assert_eq!(x.ok(), Some(2));

let x: Result<u32, &str> = Err("Nothing here");
assert_eq!(x.ok(), None);

let x: Result<u32, &str> = Ok(2);
assert_eq!(x.err(), None);

let x: Result<u32, &str> = Err("Nothing here");
assert_eq!(x.err(), Some("Nothing here"));
}

To do the conversion the other way around, you use either .ok_or(err) or .ok_or_else(err_closure):

https://doc.rust-lang.org/std/option/enum.Option.html#method.ok_or

https://doc.rust-lang.org/std/option/enum.Option.html#method.ok_or_else

#![allow(unused)]
fn main() {
let x = Some("foo");
assert_eq!(x.ok_or(0), Ok("foo"));

let x: Option<&str> = None;
assert_eq!(x.ok_or(0), Err(0));

let x = Some("foo");
assert_eq!(x.ok_or_else(|| 0), Ok("foo"));

let x: Option<&str> = None;
assert_eq!(x.ok_or_else(|| 0), Err(0));
}

For these, you need to provide the instance of the Error type, as you can see.

Flattening

Sometimes, it is possible to get into a situation where you have a nested result or a nested option. While nested options are far more common, you are likely to encounter both.

Instead of going through hoops with pattern matching, you can flatten an option:

#![allow(unused)]
fn main() {
let x: Option<Option<u32>> = Some(Some(6));
assert_eq!(Some(6), x.flatten());

let x: Option<Option<u32>> = Some(None);
assert_eq!(None, x.flatten());

let x: Option<Option<u32>> = None;
assert_eq!(None, x.flatten());
}

The same functionality is available for Result, but it is nightly as of yet. Keep in mind, that the E type has to match:

#![allow(unused)]
#![feature(result_flattening)]
fn main() {
let x: Result<Result<&'static str, u32>, u32> = Ok(Ok("hello"));
assert_eq!(Ok("hello"), x.flatten());

let x: Result<Result<&'static str, u32>, u32> = Ok(Err(6));
assert_eq!(Err(6), x.flatten());

let x: Result<Result<&'static str, u32>, u32> = Err(6);
assert_eq!(Err(6), x.flatten());
}

Transposition

In other cases, you might run into a situation, where you either have a result containing an option or an option containing a result, and you need the opposite.

For this, .transpose() is available on both Result and Option

#![allow(unused)]
fn main() {
#[derive(Debug, Eq, PartialEq)]
struct SomeErr;

let x: Result<Option<i32>, SomeErr> = Ok(Some(5));
let y: Option<Result<i32, SomeErr>> = Some(Ok(5));
assert_eq!(x.transpose(), y);


let x: Result<Option<i32>, SomeErr> = Ok(Some(5));
let y: Option<Result<i32, SomeErr>> = Some(Ok(5));
assert_eq!(x, y.transpose());
}

Checking the status

While it is your prerogative to match on an option or result simple to figure out its state, such as like this:

#![allow(unused)]
fn main() {
if let Some(_) = my_option {
    println!("Hello!");
}
}

This is considered an anti-pattern, and you are much better off using the .is_some() method:

#![allow(unused)]
fn main() {
if my_option.is_some() {
    println!("Hello");
}
}

For result, there is once again two methods for checking for value and error each:

The try operator

With many operations being fallible, it can be handy to terminate execution early, returning an error in case an operation fails.

In Rust, this was previously done via the try!() macro, which is now considered obsolete, and nowadays, is done using the ? operator, known as the try operator.

The try operator is in no way special, or exclusive to Result and Option, you can implement the std::ops::Try trait on any type you desire.

Some implementors, such as the ones found in the anyhow error-handling crate feature conversions from both Option and Result with any Error type, allowing for maximum ergonomics.

#![allow(unused)]
fn main() {
fn find_char_index_in_first_word(text: &str, ch: &char) -> Option<usize> {
    let first_word = text.split(" ").next()?;
    let index = first_word.find(|x| &x == ch)?;

   Some(index)
}
}

In the future, Rust will also feature try {} blocks:

#![allow(unused)]
#![feature(try_blocks)]

fn main() {
use std::num::ParseIntError;

let result: Result<i32, ParseIntError> = try {
    "1".parse::<i32>()?
        + "2".parse::<i32>()?
        + "3".parse::<i32>()?
};
assert_eq!(result, Ok(6));

let result: Result<i32, ParseIntError> = try {
    "1".parse::<i32>()?
        + "foo".parse::<i32>()?
        + "3".parse::<i32>()?
};
assert!(result.is_err());
}

A try block creates a new scope one can use the ? operator in.

As iterator

Both Option and Rust can be used as iterators of 0 or 1 elements.

#![allow(unused)]
fn main() {
let x = Some(4);
assert_eq!(x.iter().next(), Some(&4));

let x: Option<u32> = None;
assert_eq!(x.iter().next(), None);

let mut x = Some(4);
match x.iter_mut().next() {
    Some(v) => *v = 42,
    None => {},
}
assert_eq!(x, Some(42));

let mut x: Option<u32> = None;
assert_eq!(x.iter_mut().next(), None);

let x: Result<u32, &str> = Ok(7);
assert_eq!(x.iter().next(), Some(&7));

let x: Result<u32, &str> = Err("nothing!");
assert_eq!(x.iter().next(), None);

let mut x: Result<u32, &str> = Ok(7);
match x.iter_mut().next() {
    Some(v) => *v = 40,
    None => {},
}
assert_eq!(x, Ok(40));

let mut x: Result<u32, &str> = Err("nothing!");
assert_eq!(x.iter_mut().next(), None);
}

Panics! and alternatives

If you do not care about handling (and/or propagation) of an error, or the lack of a value, there are two methods you can use to quickly extract the underlying value:

  • .expect("msg")
  • .unwrap()

The first one will include a message of your choosing in the resulting panic and stacktrace:

#![allow(unused)]
fn main() {
let x: Result<u32, &str> = Err("emergency failure");
x.expect("Testing expect"); // panics with `Testing expect: emergency failure`
}

Whereas .unwrap() will use a default message for the particular container type:

#![allow(unused)]
fn main() {
let x: Result<u32, &str> = Ok(2);
assert_eq!(x.unwrap(), 2); // ok

let x: Result<u32, &str> = Err("emergency failure");
x.unwrap(); // panics with `emergency failure`
}

There are more unwrapping methods that do not panic:

  • .unwrap_or_default(), which is available for T: Default will return the default value in case the result is an error or the option is none
  • .unwrap_or(alt) will return the alternative value your provided
  • .unwrap_or_else(|| produce_alt()) will return an alternative value constructed with the closure or function you provide. Use this if creating the value is expensive. In the case of Result, the closure takes a single parameter containing the error.

Finally, in the case of Result, if you are looking for the error in particular, you can use .unwrap_err().

Taking references

It is sometimes handy to take references to the contents of an option or a result without having to deconstruct it.

For this, option and result provides .as_ref() and .as_mut() methods, which transform a reference to an option/result of T into an option/result of &T (and optionally &E)

#![allow(unused)]
fn main() {
let x: Result<u32, &str> = Ok(2);
assert_eq!(x.as_ref(), Ok(&2));

let x: Result<u32, &str> = Err("Error");
assert_eq!(x.as_ref(), Err(&"Error"));

fn mutate(r: &mut Result<i32, i32>) {
    match r.as_mut() {
        Ok(v) => *v = 42,
        Err(e) => *e = 0,
    }
}

let mut x: Result<i32, i32> = Ok(2);
mutate(&mut x);
assert_eq!(x.unwrap(), 42);

let mut x: Result<i32, i32> = Err(13);
mutate(&mut x);
assert_eq!(x.unwrap_err(), 0);
}

Other handy methods

The same methods exist for Option as well.

Note that some of the iterator-ish methods exist directly on types as well, such as map or filter

The task: Disjoint function

For this exercise, write functions doing the following conversions:

  • fn<E> Result<String, E> -> Option<i32> which tries to convert a string into an i32
  • fn<T> a, b, c: Option<T> -> Iterator<Item=T> which takes three options and returns an iterator over all of them
  • fn<T, E> Option<Result<Option<T>, E>> -> Option<Result<T, E>> doesn't modify the internal value of type T in any way
  • fn<T, E> Option<Result<Option<T>, E>> -> Result<Option<T>, E> same as previous
  • fn<T, E> Option<Result<Option<T>, E>> -> Option<T> same as previous, error is ignored

Final product

In the end you should be left with a well prepared project, that has the following:

  • documented code explaining your reasoning where it isn't self-evident
  • optionally tests
  • and an example or two where applicable
  • clean git history that does not contain fix-ups, merge commits or malformed/misformatted commits

Your Rust code should be formatted by rustfmt / cargo fmt and should produce no warnings when built. It should also work on stable Rust and follow the Braiins Standard