10

Polishing Rust. Boxing and Unboxing Results | by Tim Keating | Jan, 2021 | Mediu...

 3 years ago
source link: https://mrtact.medium.com/polishing-rust-30eeac3c4bf3
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Polishing Rust

I recently wrote this nasty piece of code:

What annoys me about this is that it feels unhygienic. Like in my last article, I’m doing way too much work. The clunkiness of having to match everything against a Result, only to then re-wrap each output in the correct variant, completely obscures the beauty of that match expression. I mean, seriously, look at it! Not blowing my own horn — I didn’t have to be all that smart to write it — I’m simply pointing out the expressiveness of Rust pattern matching. Even as I was writing this, I was thinking “For the love of all that is holy, there has to be a better way to structure this.” And of course, there is. But I had no idea where to even look.

Then I watched this excellent techniques video by Nicholas Cameron. If you’re an intermediate Rust programmer looking for some good insights into why certain language features work the way they do, and for some good pointers on interesting and useful stdlib features that merit a deep dive, it’s well worth your time.

In the video, he happened to mention [Result|Option]::map, and it immediately clicked that this was what I needed. A quick refactoring and I managed to clean up all those extra Oks.

First let’s talk a bit about what this code is doing. It uses the graph_ahead_behind method of the git2::Repository object to compare two branch objects and return the number of commits between them. That call returns a Result<(usize, usize), E>. My function matches on the result, cracking the inner 2-tuple into the correct variant of the AheadBehindenum and rewrapping the Err value with a useful-ish error message. If there is no upstream branch, it returns AheadBehind::NotTracking.

The elusive AheadBehind enum, observed in the wild.

Brief aside: Yes, you could argue that this is a bit over-engineered and I could just pass around the 2-tuple. However, one of the things I really like about Rust is the power and flexibility the type system gives you for modeling data in exquisite detail. I now have states that represent every meaningful combination of ahead/behind data enforced by the type system(including two different “None”-equivalent variants that mean different things). Even though I’m just going to call this enum’s Display trait implementation, which is going to do a similar match to unpack this data into a string representation, I find it worthwhile to make it unambiguous what the possible states are and to make it easy to reason about what’s going on. YMMV.

So to clean this up, we’re going to use map on the result returned from graph_ahead_behind. map allows you to apply a function or closure to the contents of a successful Result, and will then return your output repackaged in Ok, so you don’t have to do it by hand.

Brief aside #2: Why do I use the letter ‘o’ for my generic closure param name? I used to use ‘it’ because I came to Rust from Kotlin, where you can omit the param if you want, and it will be named ‘it’ by default. However, I recently realized that since Rust uses pipes to enclose a param block, using ‘o’ makes the block look like a TIE fighter. I am not a terribly serious person.

The big change here is on the inner block. We just make the call to graph_ahead_behind and then call map on the return value, moving the match expression inside the newly-added closure. Now we can match cleanly on the raw contents of the result, and whatever we return from the closure will automagically get wrapped in Ok, eliminating the need for all those repeated calls in the match arms.

As an extra-special added bonus, map will forward any Err result for us, so there’s no need to explicitly handle that case. However, I don’t want to lose that bit of info about what the code is trying to do there, so I will add an anyhow::Context to the result of the ahead/behind call.

In fact, we can go one step further, using a similar tactic with map_or to handle the outer Result. Let’s make both of those changes:

Ahh, much nicer!

If you’re wrestling with learning Rust, I hope you find this helpful. If you have some Rust code that you feel is suboptimal but you just can’t see how to make it better, hit me up on Twitter at @mrtact.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK