Deal with it — Option type in Rust

4 min read Original article ↗

Steve Robinson

After much reading, Rust started to really attract us into it.

Something that really awed us was Rust’s enforcement on us to handle every possible scenario.

Press enter or click to view image in full size

Let’s say we’re trying to find the index of a string within an array.

Here’s what one would do in Ruby —

["Adam", "Josh", "Jackson"].find_index("Jackson") #=> 2

All good! What if the element is not in the array?

["Adam", "Josh", "Jackson"].find_index("Mat") #=> nil

Yes! nil. Ruby programmers (for that matter, most other programmers who work with languages that have the concept of nothing) would not find this behaviour surprising or troublesome.

We’re used to dealing with nil values all the time in Ruby. We might do something like this —

Heck, we might not check if the returned value is nil at all like in this line of code —

["Adam", "Josh", "Jackson"].find_index("Judith").upcase

which would blow up with the very popular exception — NoMethodError: undefined method 'some_method' for nil:NilClass. There is nobody forcing the developer to handle this nil case and thus this is a very common source of errors in Ruby applications especially when method calls are chained together like foo.bar.baz.boo — which is often referred to as a train-wreck and rightly so!

Coming to Rust, let’s try to write an implementation for find_index (assuming we’re only dealing with a vector of strings) —

From the function declaration we can tell this takes in a&[String] and a String type and would return an usize which will have the index of the element in the array.

But as such this function would not compile because of the super cool type checker that the Rust compiler employs. This is what it spits out —

error[E0308]: mismatched types
--> example.rs:7:3
|
7 | for (i, name) in names.iter().enumerate() {
| ___^ starting here...
8 | | if name == element {
9 | | return i;
10 | | }
11 | | }
| |___^ ...ending here: expected usize, found ()
|
= note: expected type `usize`
= note: found type `()`

The way the error messages talk to us is yet another thing we really loved about Rust.

Rust is able to figure out that the function of ours has a possible return value of an empty tuple — () (which is what Rust functions return when we do not return anything explicitly). Taking a look at the function again, we could figure out that this is what happens when the element is not found.

In Ruby, we got away with returning a nil, but in Rust we are forced to declare a return type for our function and we need a type that would cover both a possible match (i32 index) and the possibility that the operation failed.

This is where the Option type comes in. An Option type is basically an enum in Rust who’s values can either be one of —

  1. Some — representing existence of a value and it contains the value as well.
  2. None — representing absence of a value or simply nothing.

Let’s rewrite our function to return an Option type instead of a usize like so —

And at the place where we’re using this function, we’ll use Rust’s pattern matching to branch out like so —

In addition to this simple pattern matching approach to deal with Option returns, Rust provides us with a number of other techniques.

One of those techniques is using theunwrap() function. Calling unwrap on an Option would, in case of a Some, simply return the value and in case of a None, would panic (the program terminates abruptly). The following would panic —

find_index(names, name_not_in_names).unwrap();

Instead of panicking, if you’d like to return a default value if the Option is a None, you could use unwrap_or(default_value).

We’re ending this article by looking at yet another method provided by Rust to deal with Options and its the map function. (technically a combinator, which we’ll discuss in another post).

map simply transforms one Option<T> to a different Option<U> using the given function. It returns None if Option on which it was called is a None itself. This lets us compose functions cleanly like so —

find_index(names, element).map(|i| i.to_string());

Expressing operations by means of composing functions, without maintaining any state along the way, is a beautiful way to write code — one which is encouraged by the Rust language.

Thus Rust forces the developer to deal with the situation where their function might not return a proper value and the caller of the function to handle the returned Option as well. All this leads to a very stable and self-documenting code.

In another post we’ll deal with a closely related sibling — Result. Till then, ciao!

Adventures in Rust is a series of posts where my Parter-in-Science,

and I write about our experience building a webserver in Rust.

Incase, you want to see how our project is turning out, here’s a secret link to our Github repo. **BIG SMILES**