I lost an entire Saturday to a data structure that would have taken ten minutes in any garbage-collected language. The structure was a humble tree where nodes needed to refer back to their parent. In Go I'd have a pointer up and a slice of pointers down and not give it a second thought. In Rust the compiler looked at my back-reference, raised an eyebrow, and asked me who exactly owned what.
That's the borrow checker's whole job, and it is relentless. Every value has one owner. You can lend it out immutably to many readers, or mutably to exactly one writer, never both at once. A child holding a reference to its parent while the parent holds references to its children is precisely the kind of ownership cycle it refuses to let you express casually, because that cycle is where use-after-free and double-free bugs live in C.
My increasingly silly workarounds
I tried everything to win the argument. I sprinkled lifetimes until the signatures looked like line noise. I cloned things I had no business cloning, turning a tree into an expensive forest of copies. At one low point I reached for unsafe and raw pointers, which is the Rust equivalent of arguing with the referee by setting fire to the pitch. It compiled. It also felt like cheating, and worse, like cheating badly.
The turning point was admitting the compiler wasn't being awkward, it was asking a real question I'd been avoiding. What is the lifetime of a parent relative to its children, actually? When a node is removed, who's responsible for the dangling reference the children still hold? In every GC language I'd just let the runtime worry about it. Rust was forcing me to have an opinion, up front, in the type system.
Losing properly
So I stopped fighting and reached for the standard answer to shared ownership: Rc for the shared parts, with Weak for the back-references so the cycle doesn't leak, and RefCell where I genuinely needed interior mutability.
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
A child holds a Weak reference up to its parent, which doesn't keep the parent alive, so there's no reference cycle and nothing leaks. The parent holds strong Rc references down to its children. Borrow checking moves to runtime via RefCell, which means I can now panic at runtime if I borrow it wrong, but at least the design is honest about who owns whom.
It's not the prettiest code I've written, and borrow_mut() everywhere is a tax. But it works, it doesn't leak, and crucially I now understand why the naive version was a bug waiting to happen rather than a clever shortcut. That's the trade the borrow checker keeps offering: a worse Saturday in exchange for a better Tuesday at three in the morning. I lost the fight. I think I came out ahead.