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
- Learning Rust #1: Pattern Matching
- Learning Rust #2: Option & Result (you are here)
- Learning Rust #3: crates.io & publishing your package
- Learning Rust #4: Parsing JSON with strong types
- Learning Rust #5: Rustlings
- Learning Rust #6: Understanding ownership in Rust
- Learning Rust #7: Learn from the community
- Learning Rust #8: What's next?
- Learning Rust #9: A talk about rustlings
- Learning Rust #10: Added new feature with a HashMap
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 Option
s:
// 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 Result
s
than with Option
s:
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.