Juha-Matti Santala
Community Builder. Dreamer. Adventurer.

Learning Rust #2: Option & Result

Last December I finally started learning Rust and in January I built and published my first app with Rust: 235. Learning Rust is my new monthly blog series that is defnitely not a tutorial but rather a place for me to keep track of my learning and write about things I've learned along the way.

Learning Rust series

Last month I started this series by talking about something I really enjoyed: pattern matching. And while writing and after sharing that post, I did further refactoring on the codebase based on ideas and suggestions by the community. Special thanks to Juho F. for helping out!

This month, I want to talk about something that made me struggle a lot and caused (and still causes) my code writing to slow down considerably: Option and Result types and how I constantly run into issues with them.

Coming from Python and Javascript development, I'm used to values being most often just values (or null). I can call a function and immediately continue with the return value.

In Rust, things are done a bit differently. You can still write functions that take an input and return a value but in many cases, functions return an Option or Result that are kind of wrappers around the actual values. The reason for this is to add safety over different kind of errors and missing values.

What is an Option?

Option is a type that is always either Some or None.

fn divide(numerator: f64, denominator: f64) -> Option<f64> {
    if denominator == 0.0 {
        None
    } else {
        Some(numerator / denominator)
    }
}

The above example is from Rust Option's documentation and is a good example of Option's usefulness: there's no defined value for dividing with zero so it returns None. For all other inputs, it returns Some(value) where the actual result of the division is wrapped inside a Some type.

For me, it's easy to understand why it's implemented that way and I agree that it's a good way to make the code more explicit about the ways you treat different end cases. But it can be so frustrating especially when learning programming.

There are a couple of ways to get the value from Options:

// Pattern match to retrieve the value
match result {
    // The division was valid
    Some(x) => println!("Result: {}", x),
    // The division was invalid
    None    => println!("Cannot divide by 0"),
}

Continuing from previous example and my previous blog post on the topic, pattern matching gives a clean interface for continuing with the data. I find this most useful when it's the end use case for the result.

games.into_iter().for_each(|game| match game {
  Some(game) => print_game(&game, use_colors),
  None => (),
});

In 235, I use it for example to print games that have been parsed correctly and just ignore the ones that were not. Ignoring them is not a very good way in the long run but in this case, it's been good enough.

Other way is to unwrap the Option. The way unwrap method works is that it returns the value if Option is Some and panics if it's None. So when using plain unwrap, you need to figure out the error handling throughout.

There are also other variants of unwrap. You can unwrap_or which takes a default value that it returns when Option is None. And unwrap_or_else which functions like the previous but takes a function as a parameter and then runs that on the case of None. Or you can rely on the type defaults by running unwrap_or_default if all you need is a type default.

But there's still more! For added variety, there's the ? operator. If the Option it's used on is a Some, it will return the value. If it's a None, it will instead return out from the function it's called in and return None. So to be able to use ? operator, you need to make your function's return type an Option.

To create an Option yourself, you need to use either Some(value) or None when assigning to variable or as a return value from a function.

What's a Result then?

The other "dual type" (I don't know what to call it) in Rust is Result. Similar to Option above, Result can be one of two things: it's either an Ok or an Error. To my understanding so far, Ok basically functions the same than Some that it just contains the value. Error is more complex than None in that it will carry the error out from the function so it can be taken care of elsewhere.

Same kind of pattern matching can be used to deal with Results than with Options:

match fetch_games() {
  Ok(scores) => {
    let parsed_games = parse_games(scores);
    print_games(parsed_games, use_colors);
  }
  Err(err) => println!("{:?}", err),
};

In 235, on the top level, I use above pattern match after fetching the scores from the API. If anything happens with the network request, the application will print out the error. One of the tasks on my todo list is to build a bit more user-friendly output depending on the different cases rather than just printing out raw Rust errors. I haven't figured out yet how to deal with errors in Rust in more detail yet though – once I do, I'll write a blog post in this series about it.

Like with Option, unwrap and ? also work for Result.

So why do I struggle?

As I've been writing this blog post, everything seems so clear and straight-forward to me. Still, when actually writing the code, I stumble a lot. As I mentioned earlier, I come from Python and Javascript where none of this is used.

One thing I've noticed to struggle is that I write some code, calling a function from standard library or some external library and forget it's using either Option or Result. Or I forget which one had which internals and thus, I'm still in a phase with Rust where I need to check so many things from the documentation all the time.

It also feels like you need to take so many more things into consideration. If you want to use ?, you also need to change the way your function is defined and how the caller works and sometimes that can go on for a few levels. So my inexperience with the language leads to plenty of constantly making messy changes all around and occasionally ending up in a dead end I can't dig myself out of and I end up starting over.

And while I know it's mostly about getting used to a different system, I sometimes feel so dumb when I struggle with making the code not take into account all different ways it can crash (which is a big reason Option and Result are built the way they are).

What's up with 235?

I've been using 235 myself daily since the first developmental version and I'm loving it. It's the one piece of software I'm most proud of and it's been answering my needs very well – and from what I've heard of from others, their needs as well.

235 is about to hit version 1.0. I'm getting confident that I've taken care of most of the major bugs conserning the basic usage and that gives me confidence in incrementing the version to 1.0.

Two things still need to be taken care before that and I'll hopefully get them done early March once I get a couple of lectures and talks out of my plate. First thing is to fix a bug that has a slight chance to appear during the playoffs. Second one is to make the error handling bit better and provide more user-friendly outputs when things go wrong.

If I wanna learn Rust, where should I go?

Like I said, don't take these posts as gospel or tutorial. If you wanna learn Rust, my recommendation as a starting point (and one I started with) is Rust Book and with it, Rust by Example. Also, joining your local Rust communities and being active in Twitter by following @rustlang and taking part in discussions with #rustlang.