4 ms·
Rust Contagious Borrow Issue
- jasonthorsness 1y agoI’ve been learning Rust via the book and a great article I found on linked lists [1]. Coming from C++ the lifetimes/borrows concepts make sense at a high level but the practical details seem to get pretty crazy. If anyone here knows Rust well does the OP article have a good take or is it missing something? [1] https://rust-unofficial.github.io/too-many-lists/ https://rust-unofficial.github.io/too-many-lists/
- scottlamb 1y agoI think if you've hit this problem and are looking for solutions, this article looks like a helpful read. There are lots of ideas there. I wouldn't say this is a super common problem (though I have hit it). The opening example here is that logic outside `Parent` is maintaining its summary state based on its children. That's unusual; typically `Parent` itself would be responsible for that, and so you can inline the logic without having to expose the fields. Sometimes inlining the logic gets impractical though if the logic is super long. In that case it can be helpful to split it into sub-structs so that you can easily call a method on a group of fields. I did that here, for example: <https://github.com/scottlamb/moonfire-nvr/blob/ff383147e4ff7e2d9b956b3798d8a6013fcfefe5/server/src/mp4.rs#L647 https://github.com/scottlamb/moonfire-nvr/blob/ff383147e4ff7...> There have been language proposals to define "view types" which are basically groups of fields that are borrowed. <https://smallcultfollowing.com/babysteps/blog/2021/11/05/view-types/ https://smallcultfollowing.com/babysteps/blog/2021/11/05/vie...> IMHO, they're not worth the extra language complexity.
- recursivecaveat 1y agoThis actually seems like a very good collection of strategies. The only one I use that I see missing is converting a closure capture into an argument. If you design something like: zebra.onmove(|| log(barn.contains(zebra))), then you will find everything locks up due to the references of the closure. Instead you convert the data to args: zebra.onmove(|world, zebra| log(world.barn.contains(zebra))). Obviously with cheap data which you can freeze and copy like a BarnId it's fine to do that. In general, "stop, drop, reacquire" is a good motto. ie finish figuring out what you want to happen, release the resources that you needed to figure that out, reacquire exactly the resources you need to make the thing happen, do it. That's basically the premise of 'mutation-as-data'.
- qouteall 1y agoThanks for suggestion.
- deleted 1y ago[deleted]
- phibz 1y agoFor this example id probably accumulate the score total in a local variable. Then once iterating over all the children i would call parent.add_score() with the accumulated total
- qouteall 1y agoI added clarification (That simplified example is just for illustrating contagious borrow issue. The *`total_score` is analogous to a complex state that exists in real applications*. Same for subsequent examples. Just summing integer can use `.sum()` or local variable. Simple integer mutable state can be workarounded using `Cell`.)
- bestouff 1y agoThis article says that the borrow checker doesn't look past functions signatures because of compiler performance. I strongly disagree. The reason is to avoid coupling. If it did, you couldn't swap 2 functions with the same signature because their implementation would have a different borrowing pattern. Very bad. (Although we're a bit there with functions returning an impl)
- qouteall 1y agoI added that into article
- aw1621107 1y agoIt's a bit of both IIRC. You're right that limiting checks to the function signature avoids accidentally leaking implementation details, but it also means that checking functions can be done entirely locally. Not having to recursively inspect called function implementations to determine whether there is a type/borrow checking error scales much worse than only needing to look at function signatures.
- stuaxo 1y agoIt sounds analogous to a memory leak, "borrow leak" might bea way of thinking of this.