Composition instead of inheritance

So here's "composition instead of inheritance". In C++, you can call a method in a parent class.
In Rust, you're supposed to enclose the parent struct in the child struct. But in Rust, you can't reach the parent in the child.

// So an Outer contains an Inner
struct Outer {
   val: u32,
   inner: Inner
}

impl Outer {
     // Outer has a member function
     fn hello_from_outer(&self) { println!("Hello from Outer, val = {}", self.val); } // a member function
}

// Here's Inner, which doesn't do much
struct Inner { }

impl Inner {
   pub fn call_outer(&self) {
       self.somehow_find_my_outer().hello(); // if only
   }

}

Is there some way to do this simple case short of some hack involving lots of Rc and Weak links?
(And no suggesting 'unsafe')

The straightforward solution would be to pass the Outer into Inner::call_outer(), seeing as Inner has a dependency on Outer. Possibly introducing an internal trait or passing a closure if you want to inject that dependency but not have Inner referring to Outer directly.

I tried doing this with Outer is a trait object, but couldn't make that work. That's what I really want. The goal is that Inner is part of a library crate, and can be used by various Outer classes. Can that be made to work, either as a generic or as a trait?

That might be promising. Example?

It all depends on the wider context.

If the Outer method you want to call takes &mut self then you are out of luck (self.inner.call_outer(&mut self) is not going to work). In which case, you might have better luck storing the state from Outer next to Inner instead of using a has-a relationship.

Another technique to deal with the shared XOR mutable aspect is to split the borrows.

Here's a toy example which splits the borrows and passes in a closure.

#[derive(Debug)]
struct Outer {
    counter: usize,
    inner: Inner,
}

impl Outer {
    fn hello_from_outer(&mut self) {
        let Outer { counter, inner } = self;
        inner.call_outer(|delta| *counter += delta);
    }
}

#[derive(Debug)]
struct Inner(usize);

impl Inner {
    fn call_outer(&mut self, add: impl FnOnce(usize)) {
        add(self.0);
    }
}

fn main() {
    let mut outer = Outer {
        counter: 0,
        inner: Inner(42),
    };

    println!("Before: {:?}", outer);
    outer.hello_from_outer();
    println!("After: {:?}", outer);
}

(playground)

Before: Outer { counter: 0, inner: Inner(42) }
After: Outer { counter: 42, inner: Inner(42) }
1 Like

Composition over inheritance means instead of making the Specific inherits from the Generic, make the Specific a composition with the Generic. In the OP's example the Outer corresponds to the Specific and the Inner corresponds to the Generic, and it's trying to call the child class' additional method from the parent class.

2 Likes

Hm. So that's the closure approach. That's a big help.

The playground example can be simplified a little. See

It's not really necessary to decompose "self" into a new struct.

I need to think more about

fn call_outer(&mut self, add: impl FnOnce(usize)) {
    add(self.0);
}

I had no idea you could use impl in a type context. But it's in the standard.

Here, the instance of FnOnce is created, used, and discarded on each call to call_outer, correct? "Instances of FnOnce can be called, but might not be callable multiple times. Because of this, if the only thing known about a type is that it implements FnOnce, it can only be called once."

Now, all this works because main calls Outer, which calls Inner, which calls Outer. So we passed through Outer on the way to Inner, and can keep references to it as you pass through.

But if your main route to Outer from Inner is via a ref in Inner, this won't help.

More later.

1 Like

This is often referred to as "impl Trait". Normally you'll see it as the return value so you can hide the concrete type being returned (e.g. fn iter(&self) -> impl Iterator<Item = &'_ str> instead of your custom iterator or the unreadable mess of Map<Map<Filter<...>> you get when chaining iterators).

In this case, I used it as a shorthand for fn call_outer<F>(&mut self, add: F) where F: FnOnce(usize) because it let me keep things on one line.

Yeah, essentially.

The closure only gets called once, so by accepting an impl FnOnce we can be used in more places. For example, the closure being passed in might consume a captured variable by value, mutate it (like we did here), or not capture any state at all.

Here, the actual object that gets passed as the add argument will be a compiler-generate struct something like this:

struct Closure<'a> {
  counter: &'a mut usize,
}

impl<'a> FnMut<(usize, )> for Closure<'a> {
  type Output = ();

  fn call_mut(&mut self, args: (usize, )) -> Self::Output {
    *self.counter += args.0;
  }
}

(note: FnMut instead of FnOnce because the compiler inferred our Closure doesn't consume self.counter in its body)

The standard library also has a blanket impl which lets us use use a FnMut as a FnOnce.

Correct.

I just wanted to make it explicit... Plus older versions of Rust would see the closure passed to self.inner.call_outer(|n| self.count += n) as capturing &mut self at the same time we are using &self.inner and raise a borrow checker error.

It might be easier if you post an example, but in general I would structure the code to be "tree-like" with no backlinks. Having a reference cycle where Outer owns an Inner which contains a reference back to the parent Outer is going to give you a hard time.

Your &Outer reference essentially pins the parent in memory (moving a value counts as consuming it, and you can't consume a value when there are outstanding references - i.e. the one held by Inner). Then to make things worse, if Inner holds a &mut Outer reference you will never be able to use the Outer again because that reference "locks" the Outer until it is destroyed.

You've probably seen the name for this before - self-referential struct.

I'm well aware of that. What I want to do is this. I have a library crate. It exports a struct. Users of the crate include an instance of that struct in their own struct. The crate struct is mostly called by from the user side, but sometimes it needs to call back to the user and get some info from the user's struct.

This is exactly what happens when you subclass a library class in C++, of course. That's the functionality I need. I've been able to avoid this situation up to now, but now I'm facing it.

This is correct answer. What you're trying to do with your code is accessing the child from the parent.

Rename your structs as Parent and Child to get a more concrete example.

And note that there are very similar word exist: callback. That's really what you need (as opposed to want).

Correction: that's what you want, not need.

Not the biggest mistake C++ ever did, but a serious mistake, anyway. This makes these callbacks from parent to child implicit, undocumented and almost unnoticeable. There are many schools which try to prevent the whole thing from becoming an undebuggable mess, but Rust picks one extreme solution: that's not allowed, period.

If you have to have callback — provide it explicitly.

Try to explain just why do you need it and people would, probably, be able to help you.

But Rust doesn't include a way to create the usual implementation inheritance mess — on purpose.

3 Likes

FnMut works there, too. So the callback can be reused. That's closer to what I need. That's very helpful. I didn't think that would work. I was expecting borrow trouble, since Inner has, indirectly, hold of a reference to Outer.

Because there are multiple threads involved, this solution won't work in my real program. What I'm going to do, instead, is get rid of the callback entirely, and have the child keep a cache of the data the parent provides, rather than asking the parent for it. Somebody has to cache it anyway, so this is just moving the cache from parent to child. Requests for items not in cache will go out on a channel from child to parent, which was going to happen in the parent anyway. This is more in line with the usual ways of doing things in Rust.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.