I spent most of Saturday trying to build a small dependency graph in Rust, and most of Saturday losing to the borrow checker. I want to be honest about how that went, because the honest version is more useful than the triumphant one.
The shape was simple. Nodes, edges, a way to walk from a node to its dependents and mutate them. In any language I had written for the previous decade this is twenty minutes of work. In Rust it was an afternoon of cannot borrow as mutable because it is also borrowed as immutable, which is the compiler's polite way of saying "you are about to do something you will regret".
My first instinct, which I am not proud of, was to reach for the workarounds. A bit of Rc. Then a RefCell inside the Rc when the Rc alone would not let me mutate. Then a moment of clarity where I realised I was reconstructing, by hand and badly, the exact aliasing-plus-mutation situation the borrow checker exists to stop. I was not defeating it. I was smuggling the problem past it into runtime, where instead of a compile error I would eventually get a RefCell panic at two in the morning.
So I stopped fighting and asked what the compiler actually wanted. It wanted me to admit that "a node holds references to other nodes, and I mutate them while holding those references" is genuinely ambiguous. If A points at B and B points back at A, who owns whom? While I am editing A through a reference reachable from B, what stops me corrupting B's view of A? In C++ the answer is "nothing, good luck". The borrow checker simply refuses to let me pretend that question has an easy answer.
The fix was not clever. I stopped storing references in the nodes and stored indices into a single Vec that owns all of them.
struct Graph {
nodes: Vec<Node>,
}
struct Node {
name: String,
deps: Vec<usize>, // indices into Graph::nodes, not &Node
}
Now the Vec owns every node outright, there is exactly one owner, and an "edge" is just a usize. Walking the graph means indexing. Mutating a node means taking &mut self.nodes[i] for as long as I need it and no longer. The cyclic-ownership problem evaporates because there is no ownership cycle, only a flat list and some integers that happen to refer into it. It is, embarrassingly, how you would lay this out in C if you cared about cache behaviour anyway.
I lost the argument with the borrow checker on Saturday. But "losing" turned out to mean it talked me out of a design that would have aliased mutable state through a reference cycle, which is precisely the class of bug I have spent years chasing in other languages with a debugger and a sinking feeling. It did not teach me a Rust trick. It taught me that the thing I was reaching for was wrong, and it taught me at compile time, on a quiet Saturday, instead of in production.
I still find it irritating in the moment. I have made peace with that. The irritation is the compiler doing its job, and most of the time the graceful move is to stop pushing and listen.