Last December I finally started learning Rust and last month I built and published my first app with Rust: 235. Learning Rust is my new monthly blog series that is definitely 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.
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 a 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.
Top comments (2)
I think the phrase you're looking for instead of "dual type" is "sum type". Sum types are widely used in functional programming languages and are an example of an algebraic data type (ADT). The other common type of ADT is the "product type" which corresponds to structs in Rust.
Stick with it. This stuff didn't really click with me either until I started working with Elixir and OCaml. Thanks for sharing your experience!
Thanks, sum type is good to know, that helps me also study more on them!