cherrykitten.dev/content/blog/rust_1_options_results.md

246 lines
12 KiB
Markdown
Raw Normal View History

2023-02-23 16:33:46 +00:00
+++
title = "Learning Rust Part 1: A kitten's guide to Options and Results"
date = 2023-02-25
[taxonomies]
tags=["Rust", "programming", "code", "learning"]
+++
### To unwrap() or not to unwrap(), that is the question:
So I've finally given in and started to learn Rust last month. It's a really cool programming language,
with some interesting differences to what I've used before. (JavaScript and Python, mostly)
There are some really pawesome guides out there, ["The Rust programming language"](https://doc.rust-lang.org/book/) is
definitely a **must-read** in my opinion, and [Rustlings](https://github.com/rust-lang/rustlings) is nyamazing for
anyone who likes to learn by actively working through interactive problems.
After reading through a lot of those big thorough guides by experienced Rust developers, I've started working on
my first actual Project. I approached the development of this project by just trying to get small parts of it
working in any way I can manage, and then build upon this. In that process, I learned a lot of small subtilties that
guides like the ones named above just can't really cover. This post is for sharing those things, those cool little
tips to make your first Rust project just a little cleaner and more Rust-y. Originally I wanted to make this about a lot
of different topics, but then I've realized that my notes already contain so many things about just one part of Rust:
The Enums `Option` and `Result`. So this post will be about those, and hopefully will mark the start of a series on this blog.
While reading through this, you might think that the things I'm mentioning are obvious. That's okay, and that's the point.
Nothing is ever completely obvious to everyone, and this is for those like me, who often don't immediately recognize
the "obvious". And, to be honest, I am writing this just as much for myself, writing all of that stuff down to aid me in my
own ongoing learning process.
So, let's start!
<!-- more -->
Firstly, a very quick introduction. `Option` and `Result` are part of the Rust standard library. Quoting the official documentation
is probably the easiest way to summarize their purpose:
> Type `Option` represents an optional value: every Option is either `Some` and contains a value, or `None`, and does not. `Option` types are very common in Rust code, as they have a number of uses:
>
> * Initial values
> * Return values for functions that are not defined over their entire input range (partial functions)
> * Return value for otherwise reporting simple errors, where `None` is returned on error
> * Optional struct fields
> * Struct fields that can be loaned or “taken”
> * Optional function arguments
> * Nullable pointers
> * Swapping things out of difficult situations
and
> `Result<T, E>` is the type used for returning and propagating errors. It is an enum with the variants, `Ok(T)`, representing success and containing a value, and `Err(E)`, representing error and containing an error value.
At first, it seems so easy to just add a quick `.unwrap()` after every `Option` or `Result`, but this comes with
the disadvantage of your code [panicking](https://doc.rust-lang.org/std/macro.panic.html) if it fails to unwrap.
Sometimes, this can be useful during development, to discover potential error cases you might not have thought about, but
is usually not what you want to happen.
So, what can you do instead?
First of all, don't use `unwrap()` unless you are completely sure that the value will never panic. Sometimes that is the
case, because an earlier part of your code already made sure that it is `Ok` or `Some`.
In some cases, you actually want the program to panic. But even then, there is a slightly better way. You can use `expect("foo")`
to add a message to the panic, so the user actually knows what went wrong. That message should be worded in a specific way,
basically telling the user what you _expected_ to happen.
```rust
fn main() {
let x = Some("Hello, World");
// There is no actual "None" type/value in Rust,
// this "None" is more specifically Option::None
let y: Option<String> = None;
let z = x.unwrap(); // We explicitly set this to Some, so we can safely unwrap it
let rip = y.expect("expected y to not be None");
println!("{}, {} will never print because it panics above", z, rip);
}
```
There are also the non-panicking siblings of `unwrap()`, like `unwrap_or()`, `unwrap_or_else()` and `unwrap_or_default()`.
```rust
fn main() {
let a: Option<String> = None;
let b: Option<i32> = None;
let c: Option<bool> = None;
// unwrap_or() lets you supply a specific value to use if the Option is None
let x = a.unwrap_or("Hello there".to_owned());
// unwrap_or_default() uses the types default value if the Option is None
let y = b.unwrap_or_default();
// unwrap_or_else() lets you specify a closure to run if the Option is None
let z = c.unwrap_or_else(|| if 1 + 1 == 2 { true} else { false });
assert_eq!(x, "Hello there".to_owned());
assert_eq!(y, 0);
assert_eq!(z, true);
}
```
And then there is this really cool question-mark operator, which comes in very handy once you go multiple functions deep
and keep having to work with more and more `Result`s and `Option`s. The way it works is that, if you have a `None` or an `Error`,
it passes up the handling of this one level higher, by returning out of the function early with a `None` or `Error` value itself.
Of course, since return types of functions have to be known at compile time, the question-mark operator only works inside
functions that already return `Result` or `Option`.
```rust
fn main() {
let x = 5;
let y = 10;
let z = 20;
match do_something(x, y, z) {
Some(result) => println!("Happy noises, {}", result),
None => println!("Sad noises"),
}
}
fn do_something(x: i32, y: i32, z: i32) -> Option<i32> {
let first_result = do_something_more(x, y)?;
let second_result = do_something_more(first_result, z)?;
Some(second_result)
}
fn do_something_more(x: i32, y: i32) -> Option<i32> {
Some(x + y)
}
```
The advantage of this is that you only have to handle your `None` case exactly once. You don't have to add pattern matching, or
conditionals, or `unwrap()`s all over the place, just a cute little question mark that delegates the handling to some logic
higher up.
_"But sammy!"_ you say, _"the compiler keeps shouting at me when I use the question mark on Options when my function
returns Result \`<-.\_.->´"_
Don't worry my frien! Even this has been considered!
First of all, why does the compiler get upset? It's because the question-mark operator returns the same type that it's used on,
and `Result` and `Option` are different types. Because of that, I thought I'd have to manually handle `None` cases in all
of my `Result`-returning functions. Until one day, I was reading through some documentation (I know, I know, I'm a nerd who reads
through documentation for fun and not just to find specific things) and discovered [Option::Ok_or()](https://doc.rust-lang.org/std/option/enum.Option.html#method.ok_or).
> Transforms the `Option<T>` into a `Result<T, E>`, mapping `Some(v)` to `Ok(v)` and `None` to `Err(err)`.
This was a life-changer to me, and it was just hiding right there in plain sight. Now I can easily turn a `None` where there shouldn't
be a `None` into an `Error` to pass up with my pawesome question-mark operator!
```rust
fn main() -> Result<(), String> {
let x = function_that_returns_option().ok_or("error message".to_owned())?;
// Instead of:
// let x = function_that_returns_option().unwrap();
// or any of the other ways to handle None
assert_eq!(x, ());
Ok(x)
}
fn function_that_returns_option() -> Option<()> {
return Some(());
}
```
The last thing I want to mention is both an example specific to `Option`s, and a more general tip about how I discovered this one.
There is this wonderful friend for all Rust developers, called Clippy. No, not the Paperclip from Microsoft Word, but
[A collection of lints to catch common mistakes and improve your Rust code](https://doc.rust-lang.org/stable/clippy/).
Clippy is automatically installed when you install Rust via `rustup`, and it runs a whole lot of checks against your code
to tell you what you can improve.
In my case, I had the following piece of code:
```rust
let insert = (
tracks::title.eq(match tag.title() {
Some(title) => Some(title.to_string()),
None => None,
}),
tracks::track_number.eq(match tag.track() {
Some(track) => Some(track as i32),
None => None,
}),
tracks::disc_number.eq(match tag.disk() {
Some(track) => Some(track as i32),
None => None,
}),
tracks::path.eq(match path.to_str() {
None => return Err(Error::msg("Could not get path")),
Some(path) => path.to_string(),
}),
tracks::year.eq(match tag.year() {
Some(year) => Some(year as i32),
None => None,
}),
tracks::album_id.eq(match album {
Some(album) => Some(album.id),
None => None,
}),
);
```
This code builds an insert statement for the database holding my music metadata, getting the values from the tags of a file.
The tag fields are all `Option`s, since the tags might be empty. The databse entries are also all `Option`s, (at least on the Rust side,
on the database they are just values marked as possibly being Null). So my intuitive idea to build this was to just go through all the entries,
match the tag, put in `Some(value)` if there is a value, and `None`if there is none.
It works, it's not wrong, but there is a cleaner and more readable way to do this. And clippy told me right away, I ran it
from my IDE, and it told me:
> Manual implementation of `Option::map`
Huh, okay. Let's check the [documentation](https://doc.rust-lang.org/std/option/enum.Option.html#method.map)
> Maps an `Option<T>` to `Option<U>` by applying a function to a contained value.
So basically exactly what I did with those `match` statements!
My IDE even had a button to just easily fix this automatically with one click:
```rust
let insert = (
tracks::title.eq(tag.title().map(|title| title.to_string())),
tracks::track_number.eq(tag.track().map(|track| track as i32)),
tracks::disc_number.eq(tag.disk().map(|track| track as i32)),
tracks::path.eq(match path.to_str() {
None => return Err(Error::msg("Could not get path")),
Some(path) => path.to_string(),
}),
tracks::year.eq(tag.year().map(|year| year as i32)),
tracks::album_id.eq(album.map(|album| album.id)),
);
```
Great, that looks a lot cleaner immediately!
Note how one of the lines was not changed, that's because that one sets a DB value which is `NOT NULL`, thus if the original `Option` is
a `None` it means something went wrong, and we should abort this insert and return with an Error.
And with that, we're done with my first blogpost about Rust, with hopefully many more to come!
As I said, I am still learning, and writing this is part of my learning process. That being said, if you find this interesting,
learned something from it, etc., feel free to leave me some feedback! I'd love to hear what you think!
And if I made mistakes, please also tell me. I'm always happy to learn more and to fix those mistakes so others can learn from them too.
Thank you so much for reading 💜