Root structs? Getting roots from deeper calls?

In the past I did a lot of programming with Java, Swift, and other languages where OOP object-graph soup was allowed and easier. I'm re-thinking how to build an app in Rust. Even Arc<Mutex<Thing>> doesn't really work like a reference to Thing in those languages, because the mutable lock is not reentrant, so in Rust it seems you need to be aware of what's further up the call stack.

It seems that when I call functions, Rust makes me think more about what those functions need, and pass everything in, with the appropriate ability to mutate, or not, those arguments. I guess this is the way.

An example: I've created a UI widget tree for an app and now one of my root objects (an App) gets an event from the operating system. It then looks at it's owned UI windows, which own widgets, and chooses a widget to deliver the event to. Now that widget may have an attached event handler that wants to "navigate" the UI to another screen. So that function, executing further down the call stack, needs to reach up to more "root" objects, like a Window, Screen, or Navigator and say something like nav.push( FooScreen(arg1, arg2) )

In the OO languages it would be easier to get that nav object. Maybe you walk up the tree from self toward the root App or Window struct, or maybe you grab a global singleton.

Am I on the right track here that in Rust it's better to pass in that nav controller, somehow, to the event handlers?

Since it's difficult to include everything that a function might need in parameters, in a couple places I've created a Context struct that bundles up a few things. So while event handlers in Java might look like void handleClick(Event e), in Rust I'm getting something like fn handle_click(e: Event, ctx: &mut EventContext).

I don't have a super clear question here. Just looking for comments or general ideas about this, and checking if I'm on the right track.

I feel like Rust needs a "design patterns" book, to replace some of the OOP design pattern books I read years ago.

Since you're asking a very general question, here's a very general comment: there isn't a single solution or pattern for this in Rust.

The difference with Rust that I notice (compared to GC'd languages where everything is a reference) is that you need to design the sharing of objects for each specific situation, and try to minimize sharing as much as possible. What that means is that you should expect to refactor as things change. Refactoring is quite safe in Rust (comparatively), but it still takes time and it is good to be prepared for that.

In other words, rather than trying to find design patterns that will work no matter how complex your object graph, design for what you really need now. Then change it as it becomes more complex.

1 Like

Yes, a couple of my "context" structs, which I pass down to various functions like event handling or UI layout, have a lifetime parameter and references (shared or mutable) to pieces of larger top-level structs. This helped get around one problem I was having where I couldn't just pass around a mutable ref to the bigger top level struct.

1 Like