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

the borrow checker won, and i'm fine with it

A holiday week spent arguing with Rust's borrow checker over a tree structure, and why I stopped arguing.

A screen full of Rust code

I spent two evenings this week losing a fight with the borrow checker, and I want to write it down before I forget the shape of the lesson. The task was small: a little dependency graph for a side project, nodes pointing at other nodes. In any language I'd reach for first this is fifteen minutes of work. In Rust it took most of Boxing Day.

The instinct is to give a Node a Vec<&Node> of its children and call it done. That compiles in your head and nowhere else. The moment you try to mutate one node while holding a reference to another, rustc plants its feet and tells you, with that maddening politeness, that you cannot borrow graph as mutable because it is also borrowed as immutable. It is right. It is always right. That is the part that grates.

A tangle of references, which is roughly what my graph looked like

I did what everyone does. I reached for Rc<RefCell<Node>>, sprinkled it everywhere, and felt clever for about twenty minutes until I hit a cycle and watched a borrow_mut() panic at runtime instead of compile time, which is the worst of both worlds. I'd taken the safety the borrow checker gives you for free and paid cash for a lesser version of it.

The thing that finally clicked: stop modelling pointers. The graph doesn't need each node to own a reference to its neighbours. It needs an arena. Keep all the nodes in one Vec<Node>, and let edges be plain usize indices into that vec.

struct Graph {
    nodes: Vec<Node>,
}

struct Node {
    name: String,
    edges: Vec<usize>,
}

Now there are no references to fight over. The vec owns everything, indices are just numbers, and traversal is a loop. It is less elegant than a pointer soup right up until you remember that the pointer soup didn't compile. Petgraph, which I should have looked at sooner, does exactly this under the hood.

I am told the borrow checker stops being an adversary and starts being a colleague somewhere around the third month. I'm not there yet. But I noticed something on the second evening: once it accepted my arena design, I had no class of bug left to chase. No null, no use-after-free, no concurrent mutation I'd forgotten about. The argument was the design review I'd have skipped in C.

So I lost, gracefully, and the code is better for it. I'll take that trade more often than I'd like to admit.