1

I am new to the composition approach with Rust, and I am having a hard time trying to figure out whether I could make my code more efficient / smaller.

Let us assume that I have a base struct BaseStruct:

struct BaseStruct {
   values: Vec<i32>,
}
impl BaseStruct {
    fn new() -> Self {
        Self{values: vec![]}
    }
}

Then, I define a AsBase trait to ease the composition process:

/ Trait used to do composition
trait AsBase {
   fn as_base(&mut self) -> &mut BaseStruct;
   // Note that add_val has a default implementation!
   fn add_val(&mut self, val: i32) {
       // let us assume that this method has hundreds of lines of code
       self.as_base().values.push(val);
   }
}
// Note that BaseStruct implements the AsBase trait!
impl AsBase for BaseStruct {
    fn as_base(&mut self) -> &mut BaseStruct {
        self
    }
}

Note that BaseStruct implements the AsBase trait! Otherwise, we couldn't add a value to the vector. Then, I derive the behavior of the base struct using composition:

// Derived struct and handy functions
struct DerivedStruct {
   base: BaseStruct,
}
impl DerivedStruct {
    fn new() -> Self {
        Self{base: BaseStruct::new()}
    }
}

// Derived struct also implements the AsBase trait
impl AsBase for DerivedStruct {
    fn as_base(&mut self) -> &mut BaseStruct {
        &mut self.base
    }
}

So now, I can add values to the inner vector of my derived struct using the trait method!

fn main() {
    let mut base = BaseStruct::new();
    base.add_val(1);
    
    let mut derived = DerivedStruct::new();
    derived.add_val(1); // With composition and AsBase trait, I achieve "inheritance"
}

Here you have a playground with this example.

However, what if the add_val default method was very complex and required hundreds of lines of code? Would Rust generate a different method add_val for every struct implementing the AsBase trait? Or is the compiler smart enough to detect that they can share the same function?

Let me try to be clearer: would this alternative implementation be smaller is size, as it explicitly uses the same method?

// Base struct and handy associated methods
struct BaseStruct {
   values: Vec<i32>,
}
impl BaseStruct {
    fn new() -> Self {
        Self{values: vec![]}
    }
    fn add_val(&mut self, val: i32) {
    // Let us assume that this method is hundreds of lines long
        self.values.push(val);
    }
}

// Trait used to do composition
trait AsBase {
   fn as_base(&mut self) -> &mut BaseStruct;
   // Note that add_val has a default implementation!
   fn add_val(&mut self, val: i32) {
       self.as_base().add_val(val);
   }
}
// Note that BaseStruct does NOT implement the AsBase trait to avoid naming collision!

// Derived struct and handy functions
struct DerivedStruct {
   base: BaseStruct,
}
impl DerivedStruct {
    fn new() -> Self {
        Self{base: BaseStruct::new()}
    }
}

// Derived struct also implements the AsBase trait
impl AsBase for DerivedStruct {
    fn as_base(&mut self) -> &mut BaseStruct {
        &mut self.base
    }
}


fn main() {
    let mut base = BaseStruct::new();
    base.add_val(1);
    
    let mut derived = DerivedStruct::new();
    derived.add_val(1); // With composition and AsBase trait, I achieve "inheritance"
}

(Also, note that I couldn't implement the AsBase trait for BaseStruct due to naming collisions, I don't know if there is a workaround to avoid this other than changing the names of the methods).

Here you have a playground for the alternative version.

Román Cárdenas
  • 492
  • 5
  • 15

1 Answers1

2

Yes, the compiler will generate a new instance of add_val() for each type. It may collapse them if they use the same machine code, but it will not if they don't. If you want to avoid that, a common way is to define a nested function (see Why does std::fs::write(...) use an inner function?):

fn add_val(&mut self, val: i32) {
    fn inner(this: &mut BaseStruct) {
        // let us assume that this method has hundreds of lines of code
        base.values.push(val);
    }
    inner(self.as_base());
}

However, what you're doing is not composition. Rather, it's emulating inheritance with composition. When using the composition principle, you should not (usually) have an AsBase trait because you should not treat DerivedStruct as BaseStruct. This is not "is-a" relationship, this is a "has-a" relationship. If a method needs a BaseStruct, pass it a reference to the field directly and let it perform its work.

Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77