Polishing Rust
Boxing and Unboxing Results
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 Ok
s.
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 AheadBehind
enum and rewrapping the Err
value with a useful-ish error message. If there is no upstream branch, it returns AheadBehind::NotTracking
.
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:
Edit: In the “credit where credit is due” department, @H2CO3 on the rust-lang user forums pointed out that this may be a case where a fluent, functional style is actually harder to read than a straightforward imperative style: https://users.rust-lang.org/t/polishing-rust-boxing-and-unboxing-results/54315/3?u=mrtact. And, you know, I can’t really disagree with that.
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.