Ramblings of an aging IT geek
← Ramblings of an aging IT geek
rust

losing to the borrow checker, gracefully

A few rounds with Rust's borrow checker, why I kept losing, and the moment I realised losing was the language doing its job.

Code on a screen, the borrow checker's natural habitat

I lost an argument with the Rust borrow checker this week, and the humbling part is that it was right and I was wrong, in a way I wouldn't have noticed for months in any other language.

The pattern was innocent enough. I had a struct holding a collection, and a method that iterated over the collection whilst also wanting to call another method on self that nudged some shared state. C-brained, I wrote the obvious loop. The compiler refused, with the now-familiar complaint that I couldn't borrow self mutably whilst I was already borrowing it immutably for the iteration.

for item in &self.items {
    self.process(item); // cannot borrow `*self` as mutable
}

My first instinct, as ever, was that the compiler was being pedantic. My second, slower instinct was to ask what it was actually protecting me from. The answer: process could, in principle, mutate self.items, which would invalidate the very iterator I was walking. In C++ that's a use-after-free waiting for a quiet afternoon to ruin. The borrow checker wasn't being awkward. It was pointing at a genuine aliasing hazard I'd have shipped without a second thought.

More code, because the fight continues

So how do you lose gracefully? You stop trying to out-argue it and start restructuring so the lifetimes are honest. A few moves I reach for now, roughly in order of how much I like them:

  • Split the borrow. Often the answer is to pull the data you need out first, then do the mutation, so the two borrows never overlap in time.
  • Index instead of reference. Iterate over 0..len and index in, which sidesteps holding a reference across the mutation. Less elegant, sometimes exactly right.
  • Take, mutate, put back. std::mem::take the collection out, work on it owned, and assign it back. Crude but completely clear about what's happening.

What changed for me wasn't a trick, though. It was the realisation that every time I "fight" the borrow checker, I'm really fighting my own design, and the design is usually the thing that's wrong. The fights got shorter once I stopped treating the rejections as obstacles and started reading them as feedback. The compiler isn't blocking me from writing the program. It's blocking me from writing a subtly broken one and calling it done.

I still lose. I lost twice more after that first one, same afternoon. But I've started to enjoy losing, because each loss is a bug I didn't write, a 2am incident I won't have, found at compile time by a machine that's better at this than I am. That's a strange thing to be grateful for, and I am.