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,
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.
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.
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 likeunwrap_or_default()
orunwrap_or()
.
Not Just a Null
By now, you're probably asking yourself something similar to the following:
So why not just have
null
? What does anOption
monad provide that's more?
That's a very good question. What are the benefits of this paradigm?
- You must handle all possible returns, or lack thereof. The compiler will emit errors if you don't appropriately handle an
Option
. You can't just forget to handle theNone
(or 'null') case. - Null doesn't exist. It's immediately apparent to readers and consumers which functions might not return a meaningful value. Attempting to use a value from an
Option
without handling it results in a compiler error. - Values aren't boxed.
Option
values don't wrap pointers, they wrap values. In order to have anull
, you necessarily need a pointer. (Thanks cmr!) - Composition becomes easy. The
Option
monad becomes much more powerful when it is used in composition, as its characteristics allow for pipelines to be created which don't need to explicitly handle errors at each step.
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:
; // This could fail. (log(-2) == ??)
; // This could fail. (sqrt(-2) == ??)
;
;
;
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
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.
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.
// You can ignore these.
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.
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.
A simple pipeline. This example takes a strong and splits it into an iterator. next()
fetches the next token, which is an Option
.