Design problem in program tetrii?

This is pedantic, but it's static mut that is the problem (because it allows taking unique references that actually alias, and that's UB). An immutable static with interior mutability is fine because it disallows the mutable aliasing. One way to deal with this safely, if you really do need statically allocated state, is the once_cell crate. Which only allows initializing the static data once and then every other access is through a shared reference.

Just so that point is out of the way...

I can see how you might reach that conclusion. Just know that RefCell has the same borrowing rules as the borrow checker. It just defers the enforcement of those rules to runtime. This sometimes avoids issues that cannot be proven at compile time.

What semicoleon is showing above is that your runtime-deferred borrow checks are failing because some borrows are being held over reentrant function calls. (These are the kinds of bugs that the borrow checker can tell you about at compile time.)

Ok, I'll bite. One of the major architectural problems is that Controller is a self-referential struct: tetrii/controller.rs at main ยท russellyoung/tetrii (github.com) This is ultimately responsible for the second error mentioned in the comment above; your "Start" button acquires a unique Controller reference to call show_board() but that method in turn needs to borrow the same Controller out-of-band to create a new board reference. Similarly, Board and Controller both reference each other.

If I were to suggest an alternative design, it would be to adhere more closely to the ownership model. Only one of Board or Controller should own the other (and Controller obviously can't own itself). From the looks of it, I imagine Controller is the parent in the hierarchy, though that isn't actually clear with the shared ownership offered by Rc and the reference cycles created with RefCell.

And finally, remove the cross-cutting concerns. Board should not be able to directly update the state of Controller, but it can pass messages to Controller as described by semicoleon. Board::tick and Board::drop_tick should probably be moved to Controller since they need access to sibling Boards. Things like that. Or alternatively, break these monoliths into smaller structs which can be borrowed or cloned independently.

Another way to describe this suggestion is to make information flow only one direction, typically from top-to-bottom. Every time you need to swim upstream (i.e., referencing a parent) you need to remember how much more energy you need to put into it. Because, you know, water flows downstream and all that. Not a perfect analogy, and it isn't impossible to create these tangled hierarchies. But it most certainly is not the path of least resistance. Channels fit the ownership model because they are split into a Sender and Receiver pair; each half can be owned independently, but they give you a way to swim upstream with much less effort than self-references and reference cycles.

10 Likes