Main struct access from sub-structs (or alternative)

I've got a struct that is so large it would be unwieldy if not separated into several sub-structs across multiple files in a dedicated module. However, then each sub-struct no longer has access to the fields/methods of the main struct.

My first thought was to just put an Option reference back to the top-level struct into each struct that needed it. While that would seem to compile, I'm realizing it doesn't correctly deal with the "one mutable or any immutable" references limit.

Fortunately, only one of the sub-structs needs to be mutable, though many of the immutable sub-structs depend on the variable values from the mutable one. So I could perhaps divide the top level into mutable and immutable halves and have the mutable portion contain a reference to just the immutable half for access to its values and methods. That seems like it might be viable, but would be a larger departure from the original architecture.

Alternately, and requiring less overall refactoring, I could put the mutable sub-struct in a Mutex and leave the top level struct immutable. I think in either case I might need to have the mutable portion emit an Arced immutable snapshot of it's data for use with the other immutable sub-structs' methods. These would probably also require a mutable builder for setup which would implement Into the immutable/Mutex version for actually running a simulation.

However, before I go for any major refactoring, I was wondering if anyone knew of any design patterns for dealing with this concept? I've mocked up a very generic version of the alternate (less-refactored) version in the playground as a start, but it's not feeling like the best option... I'd like to avoid heap allocation of what will just be medium-sized collections of f64 values (so taking advantage of Copy over Arc/RwLock/Mutex if possible).

Any thoughts would be greatly appreciated!

This seems pretty nonsensical to me to try to force Rust into including a reference to the whole struct itself into a (non-recursive) field of a struct (not to be confused with including a reference into the struct itself in a struct, e.g. a reference into another field which is a more complex data-structure; self-referential structs like that can sometimes be pretty useful). Especially if it’s only to make code decompose into modules. If a method needs access to the full struct, then it should be passed a reference to the full struct as a parameter. Either you coult make a method on a BigField* type into an associated function (without self) that takes a (parent: &[mut] BigStruct) parameter or you could actually add all the relevant methods just to BigStruct itself.

Note that you can split up impl blocks over multiple modules on multiple levels:

mod m {
    pub struct S;
    mod m1 {
        impl super::S {
            pub (in super::super) // making `bar` visible for `foo` here
            fn bar(&self) {
                if false {
                    self.foo();
                }
            }
        }
    }
}
impl m::S {
    fn foo(&self) {
        if false {
            self.bar();
        }
    }
}
1 Like

You surmised correctly. This would create something called a self-referential struct (Parent contains a Child which has a reference to Parent) and these have lots of issues in Rust. For one, if you were to move the Parent by value then the Child's reference would point to where the Parent was (which is now garbage), and not where it is now.

I usually try to make sure I don't get into this problem by just keeping everything small and separate. Often you find things get unwieldy because the data isn't structured in a way that matches how it is used, so it constantly feels like you're trying to shove a round peg into a square hole.

There are also strategies for having an immutable/mutable "view" over a subset of some larger data structures. The idea is you create a temporary struct with references back to the originals and pass that struct to your function.

struct BigObject {
  name: String,
  people: Vec<Person>,
  ...
}

struct View<'a> {
  name: &'a str,
  people: &'a mut [Person],
  some_callback: &'a dyn Fn(),
  ...
}

impl BigObject {
  fn do_something<F>(&mut self, callback: F) where F: Fn() {
    let args = View {
      name: &self.name,
      people: &mut self.people,
      some_callback: &callback,
    };

    do_something_with_name_and_people(args);
  }
}

fn do_something_with_name_and_people(args: View<'a>) { ... }

I've used it in the past to thread the needle when it comes to lifetimes and borrowing, but defining new temporary structs for every way you want to access your data can get a little verbose.

1 Like

I had no idea that was possible! Thank you!! Seeing it now it makes perfect sense that you could use a path in an impl, but I would never have thought to even try it. That will probably pull me out of this rabbit hole and let my wilder ideas for solving this be put on the back burner for later use... Premature optimization be damned!

I was effectively looking for a contrived way to implement &super instead of &self. And seeing that super can move to the impl path to target the top level struct, then &self becomes exactly what I want it to be. Perfect!

This also solves a second issue I was seeing with code redundancy. The previous form had getter functions stuck at the sub-struct level. Thus to have them accessible on the main struct I would have to implement a second access for each function there as well (assuming I could even get that awkward circular reference to work), or knowingly use self.sub_struct.getter(). Now there only needs to be one top-level getter for any attribute. Hooray!