4 min read

Building a Podcast Player to Learn Rust (Part 2)

This post is part of a series. View the previous one here.

What I’ve learned about the borrow checker

Rust’s borrow checker is one of the main things people love to hate about the language. At first, I did not understand it, but thought that I did. Now, I have been humbled by how much I do not know.

A consistent them has been that the borrow checker will not let me be dumb. I will try very hard to be dumb, but that doesn’t matter; the borrow checker will not let me do it.

I have learned that it is usually better to make functions accept references instead of owned values until you are absolutely certain the function should consume the value.

I have learned that different types might not support the common workarounds for when the borrow checker won’t let you do what you want (more on traits later).

One outstanding mystery to me is annotated lifetimes. These appear in the documentation for some of the libraries I’ve used, but I still struggle to understand them. Learning this concept will be a priority for me as my journey continues.

What I’ve learned about Results and Options

I love the Result and Option types. Coming from Javascript, it is a breath of fresh air to have a better way of handling emptiness and errors than using some combination of null, undefined, or (God forgive us) empty string.

The Result type must always return either a value wrapped in the Ok type or some representation of an error. An implementation of Error is typically used in such cases. I find the Result type a very clear way of reasoning about a function which might or might not succeed.

The Option type must always return either a value wrapped in the Some type or the None type. This is a very straightforward way to represent emptiness and makes initializing empty values of any type simple.

What I’ve learned about errors

Handling errors in Rust gave me some trouble as the size of my project started to grow. This is due to the strictness of type checking in Rust, and the fact that my orchestration layer needed to handle multiple different types of errors.

To handle multiple types of Errors in Rust, it is typical to create a custom error enum that wraps the various errors you expect an orchestrator to handle.

For example, imagine a function (we will call this parent) that calls two other functions (these are the children). The children each return a Result with a different error type.

There are two ways I have tried to handle this. The first example below would handle the errors by matching each result and then wrapping its returned error in the parent’s error type.

function foo(person_id) -> Result<String, Error> {
	let mut combined_name = String::new();
	let baz = find_first_name(person_id);
	match baz {
		Ok(res) => {
			combined_name = res;
		},
		Err(e) => Err("Ooops"), // handles error individually
	}
	let zing = find_last_name(person_id);
	match zing {
		Ok(res) => {
			combined_name = format!("{combined_name} {res}");
		},
		Err(e) => Err("Ooops"), // handles error individually
	}
	Ok(combined_name)
}

A Rust function handling errors of different types individually.

In the next example, we use a CustomError enum which has been created to handle both possible error types returned from the child functions. We use the ? to succinctly consume a value or propagate an error.

function foo(person_id) -> Result<String, CustomError> {
	let baz = find_first_name(person_id)?; // propagates ErrorA
	let zing = find_last_name(person_id)?; // propagates ErrorB
	Ok(format!("{baz} {zing}"));
}

A Rust function propagating different error types to be returned as an enum.

In nontrivial applications, this will save a huge amount of time and duplicated efforts.

What I’ve learned about traits

I still haven’t used traits much, but thus far I have learned that Copy and Clone are going to be popular in my code. This probably makes me a meme, but that is all I know for now.

What I’ve learned about async (Tokio)

Apparently there has been an ongoing discussion about how to handle async in Rust. The standard library includes async features, but from what I’ve read, it’s barebones and you’ll need to write your own runtime. Most people just use Tokio.

What I’ve learned about idiomatic Rust

Reducing verbosity is the main aim of most of the idiomatic Rust examples I’ve seen thus far, and for good reason. Pattern matching combined with the prevalence of Results and Options means you’re going to want to reduce the amount of code on your screen by a lot.

The moment for me when I realized something needed to change was when I had nested about fifteen levels deep while parsing an XML file to get the episode information out of a podcast feed. On the way down, every node had to be parsed, the function for which returned an Option type which must be matched or unwrapped. Unwrapping panics, so I had to learn a better way to handle this.

There is a great collection of resources on idiomatic Rust compiled by Matthias Ender at corrode.dev and an accompanying Github repo.

What I’ve learned about GUI in Rust

At first, I thought I would try to learn a toolkit like GTK to build a GUI. Then, I realized this was a harder way with no discernible benefit. Instead, I opted for a lightweight framework that would abstract away some of the more arcane and obscure parts of writing a GUI.

While building a GUI from the lowest level pieces possible is interesting, I’m just not there yet. So I decided on Iced.rs. If it’s good enough for the folks over at Pop!_OS to use as the basis for their desktop environment’s applications, it’s good enough for me.

The Elm architecture is surprisingly familiar to me. Coming from React, it feels like using a context or Redux. The elements all still have their internal state, much like useState. In the Elm model, the components are much easier to set up and the separation of concerns is clearer.

Major takeaways

  1. Result types make error handling unavoidable and are a sane default as a return type from your functions.
  2. Idiomatic Rust exists for a reason. Follow it unless you really know what you’re doing.
  3. The borrow checker is your friend, even when it doesn’t feel like it.