A tree is a graph in which every node has at most one incoming edge. This fits Rust's ownership model perfectly: if you want mutation, then each value needs exactly one incoming "edge", where an edge can be a mutable reference or direct ownership. You can also think about code as being part of this tree. Code at the top of the main loop is "near" the values that are easily accessible. So redraw
has easy access to buffer
and game
. But if you had a method defined on game
, it wouldn't have easy access to buffer
unless redraw
passed it down as a separate argument.
Funny you should mention winit
. I've recently been messing around with a toy project using winit
and wgpu
, and I've had it on hold for a few days because I need to refactor it. See, I have textures that are currently owned by the rendering pipeline that uses them (because this is "logical"). The pipeline is what knows which texture to load and when to use it.
However, I also want live reloading: any change to an on-disk file should immediately be reflected in the running program. Currently, this means keeping all the live reload code mixed in with the rendering code, which is ugly and not clean. So I very much want to move it out into its own thing that runs in a separate pass between frames.
But with the current structure, that would mean the reload code needing to "reach in" to the inside of the render pipelines to modify the textures. And I don't expect textures to stay owned by render pipelines (that's just a convenience while experimenting). In an OO language, I would just give both the reload code and the render code references to the same texture object, but I can't do that in Rust without involving locks and shared ownership, which I make a point of avoiding wherever I can.
Basically: everything is currently a tree, but the reload code would require code running at one part of the tree to "reach down" through other systems to get at the data it needs. This is messy and hard to maintain. I want the data and code to be "near" each other in the tree, with easy and uniform access.
So the solution I'm currently avoiding sitting down and doing is ripping all the textures out of the pipeline, and "moving them up the tree" so they're next to all the winit
and wgpu
state. Then, the reload code can directly access the texture state without touching anything else. The render pipeline will then need the top level redraw
function to borrow and pass down the texture state so it can do things like actually create and look up the contents of textures when it needs to.
This is kind of what I mean by the data structure "coming out" of the code. I didn't really nail down how I wanted to do live reload, so there came a point where what that code needed was at odds with how I'd structured the data. In an OO language, I would "solve" this by throwing more references at the problem. In Rust, I have to actually solve it by redesigning the data layout. It sucks, but I do find that, in practice, you end up with cleaner and simpler code.
An anecdote: the project I ported to Rust was (before the port) borderline impossible to understand. Individual parts were pretty straightforward, but how they interacted was just a complete mess. Porting to Rust forced me to clarify everything, and the final product was code that was slightly more complex on the individual level (individual components had to deal with borrows being passed in and some occasionally large argument lists), but significantly less complex overall. The actual flow of data was almost universally: "borrow some stuff you have access to and pass it down".
(That said, I did cheat on a handful of things. Resolving IDs to names ended up getting a magic global so I didn't have to pass the table down in almost every function in the codebase. Debug logging in some places used a log behind a shared mutex, since I didn't care too much about its performance. However, these were both things that were hard to do because I didn't plan for them, and refactoring for them would have been extraordinarily painful. Sometimes you have to live with imperfection...)