Photo - Kobi Li
Photo

Option Monads in Rust

One common monadic structure is the Option (or Maybe in Haskell and other languages) type. This can be seen as an encapsulation type. Consider a function which may fail to produce a meaningful value for certain inputs. For example,

fn main () {
  // Parses a string into an integer.
  from_str::<int>("4"); // A valid input.
  from_str::<int>("Potato"); // Definitely invalid.
}

The from_str function cannot return a meaningful value for "Potato". Rust (and many other functional languages) does not have null, so what should we return? This is where an Option type becomes useful. In our example, instead of returning an int type, the from_str function returns an Option<int> type.

In Rust, the Option enum is represented by either Some(x) or None, where x is the encapsulated value. In this way, the Option monad can be thought of like a box. It encapsulates the value x, where x is any type. Rust defines an Option as such, where <T> and (T) denote that it handles a generic type, meaning that T could be an int, a str, a vec, or anything else, even other Option types.

enum Option<T> { None, Some(T) }

Value In, Value Out

Since we can see an Option as a box, or encapsulation type, we need to be able to put things into the box, or take things out.

Putting a value into an Option is delightfully simple. Simply use Some(x) or None in place of x or (an imaginary) null. Most of the time, you will receive or return an Option based on input, rather than just creating them directly. Here are some examples of different techniques.

fn main () {
    // Ways to create an Option containing an int.
    let w = Some(3i); // Something
    let x: Option<int> = None; // Nothing
    // Receive from function.
    let y = some_on_even(2); // Something
    let z = some_on_even(3); // Nothing
}

fn some_on_even(val: int) -> Option<int> {
    match val {
        // Matches an even number.
        x if x % 2 == 0 => Some(x),
        // Matches anything else.
        _                     => None
    }
}

To take something out of the Option we need to be able to extract, or "unwrap" the value. There are a number of ways to do this. Some common methods are with a match or .expect().

If you're seeking to write code that won't crash, avoid .expect() and it's cousin .unwrap() and use safer alternatives like unwrap_or_default() or unwrap_or().

fn main () {
    // Create an Option containing the value 1.
    let a_monad: Option<int> = Some(1);
    // Extract and branch based on result.
    let value_from_match = match a_monad {
        Some(x) => x,
        None    => 0i // A fallback value.
    };
    // Extract with failure message.
    let value_from_expect = a_monad.expect("No result.");
    // Extract, or get a default value
    let value_or_default = a_monad.unwrap_or_default();
    let value_or_fallback = a_monad.unwrap_or(42i);
}

Not Just a Null

By now, you're probably asking yourself something similar to the following:

So why not just have null? What does an Option monad provide that's more?

That's a very good question. What are the benefits of this paradigm?

Let's take a closer look at the composition idea...

Composing a Symphony of Functions

Nirvana is being able to compose a series of functions together without introducing a tight dependency between them, such that they could be moved or changed without needing to be concerned with how this might affect the other functions. For example, let's say we have some functions with the following signatures:

fn log(value: f64)     -> f64; // This could fail. (log(-2) == ??)
fn sqrt(value: f64)    -> f64; // This could fail. (sqrt(-2) == ??)
fn square(value: f64)  -> f64;
fn double(value: f64)  -> f64;
fn inverse(value: f64) -> f64;

Quite the little math library we have here! How about we come up with a way to turn 20 into something else, using a round-about pipeline?

sqrt(-1 * (log(-1 * (20 * 2)))^2)

With our little library it'd look something like this:

// This code will not compile, it's invalid.
// `Null` isn't a real type in Rust.
fn main () {
    let number: f64 = 20.;
    match log(inverse(double(number))) {
        x => {
            match sqrt(square(inverse(x)))) {
                y => println!("The result is {}", y),
                Null => println!(".sqrt failed.")
            }
        },
        Null => println!(".log failed.")
    }
}

In this case, we had two functions which could fail, since we didn't have an Option type, the author must be aware of and handle possible Null values. Note that the onus was on the programmer to know when a Null might be returned, and remember to handle it, not on the compiler.

Let's see what the same code would look like using the Option monad. In this example, all of the functions are appropriately defined.

fn main () {
    let number: f64 = 20.;
    // Perform a pipeline of options.
    Some(number)
        .map(double)
        .map(inverse) // Described below.
        .and_then(log) // Described below.
        .map(square)
        .map(inverse)
        .and_then(sqrt); 
    // Extract the result.
    match result {
        Some(x) => println!("Result was {}.", x),
        None    => println!("This failed.")
    }
}
// You can ignore these.
fn log(value: f64) -> Option<f64> {
    match value.log2() {
        x if x.is_normal() => Some(x),
        _                  => None
    }
}
fn sqrt(value: f64) -> Option<f64> {
    match value.sqrt() {
        x if x.is_normal() => Some(x),
        _                  => None
    }
}
fn double(value: f64) -> f64 {
    value * 2.
}
fn square(value: f64) -> f64 {
    value.powi(2 as i32)
}
fn inverse(value: f64) -> f64 {
    value * -1.
}

This code handles all possible result branches cleanly, and the author need not explicitly deal with each possible None result, they only need to handle the end result. If any of the functions which may fail (called by and_then()) do fail, the rest of the computation is bypassed. Additionally, it makes expressing and understanding the pipeline of computations much easier.

map and and_then (along with a gamut of other functions listed here) provide a robust set of tools for composing functions together. Let's take a look, their signatures are below.

fn map<U>     (self, f: |T| -> U)         -> Option<U>
fn and_then<U>(self, f: |T| -> Option<U>) -> Option<U>

Functor Interface: .map()

map provides a way to apply a function of the signature |T| -> U to an Option<T>, returning an Option<U>. This is ideal for functions like double() which don't return an Option.

This call corresponds to fmap in Haskell, which is part of a functor. Monads have this trait because every monad is a functor.

Monad Interface: .and_then()

and_then allows you to apply a |T| -> Option<U> function to an Option<T>, returning an Option<U>. This allows for functions which may return no value, like sqrt(), to be applied.

This call corresponds to bind in Haskell and theoretical Monad definitions. Meanwhile unwrapping Some<T> or None is the equivalent of return. (Thanks to dirkt)

Examples

Working with Options in Vectors. Parsing a vector of strings into integers. Note that Rust's iterators are lazy, so if collect() isn't called, the iterator itself could be composed with others.

fn main () {
    let strings = vec!("4", "12", "foo", "15", "bar", "baz", "1");
    let numbers: Vec<int> = strings.iter()
        // `filter_map` transforms `Vec<&'static str>` to `Vec<int>`
        // Any `None` will be removed,
        // while any `Some` will be unwrapped.
        .filter_map(|&x| from_str::<int>(x))
        // `collect` forces iteration through the lazy iterator.
        .collect();
    println!("{}", numbers);
}

A simple pipeline. This example takes a strong and splits it into an iterator. next() fetches the next token, which is an Option.

fn main () {
    let mut input = "15 Bear".split(' ');
    // Need to pull the number and parse it.
    let number = input.next()
        // Process Option<&'static str> to Option<int>
        .and_then(|x| from_str::<int>(x))
        .expect("Was not provided a valid number.");
    // The next token is our animal.
    let animal = input.next()
        .expect("Was not provided an animal.");
    // Ouput `number` times.
    for x in std::iter::range(0, number) {
        println!("{} {} says hi!", animal, x)
    }
}

Discussion:

Further Resources:

6c3fee57ca834cb9f133106444ebe3bf12a9628e