246 lines
12 KiB
Markdown
246 lines
12 KiB
Markdown
|
+++
|
|||
|
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 💜
|