Extending a composite Builder pattern

My goal is to implement an API that allows the end-user of my crate to construct a complex object through the Builder pattern. And to reduce complexity in a single Builder I decided to split the builder into several sub-builders, such as the user will be able to configure individual sub-aspects of the Object domain through the "enter/leave" functions entering in a sub-builder and then returning back to the context of the parent builder.

This is an example of this approach:

struct Constructor<'x> {
    x: &'x usize,
    y: usize
}

struct SubConstructor<'x> {
    constructor: &'x mut Constructor<'x>
}

impl<'x> Constructor<'x> {
    fn set_y(&mut self, y: usize) -> &mut Self {
        self.y = y;
        
        self
    }
    
    fn enter_sub(&'x mut self) -> SubConstructor<'x> {
        SubConstructor { constructor: self }
    }
    
    fn build(&self) -> usize {
        *self.x + self.y
    }
}

impl<'x> SubConstructor<'x> {
    fn double(&mut self) -> &mut Self {
        self.constructor.y *= 2;
        
        self
    }
    
    fn leave_sub(&mut self) -> &mut Constructor<'x> {
        self.constructor
    }
}

fn main() {
    let x = 50;

    let z = Constructor {x: &x, y: 0}
        .set_y(100)
        .enter_sub().double().leave_sub()
        .build();
        
    println!("{}", z);
}

(Version 1)

Works perfectly fine. But I faced an issue with the borrow-checker when I decided to implement a "Helper" extension for one of the builders:

trait Helper<'x> {
    fn double_twice(&'x mut self) -> &mut Self;
}

impl<'x> Helper<'x> for Constructor<'x> {
    fn double_twice(&'x mut self) -> &'x mut Self {
        self.enter_sub().double().double().leave_sub()
    }
}

(Version 2 - with extension)

It says that double_twice returns a value referencing data created by double_twice function, which is not true, because leave_sub() returns just &mut Self and is semantically equal to just returning a self value in this case.

How to explain borrow-checker that the leave_sub() doesn't return a value bound to SubConstructor's instance?

See the SubConstructor::leave_sub(). It takes self by reference and returns another reference, which means the returned one actually referring the SubConstructor. After the reborrow desugaring, its body will becomes &mut self.constructor.

Solution is to make all SubConstructor's methods to take self by value. Otherwise it can't move out its reference to Constructor, as we don't have full ownership of this value in the methods.

2 Likes

Types like &'a mut Type<'a> are trouble. The invariance of &mut T will often cause borrows to live longer than desired. I teased apart the two lifetimes to get this:

Which gives this error:

   Compiling playground v0.0.1 (/playground)
error[E0312]: lifetime of reference outlives lifetime of borrowed content...
  --> src/main.rs:34:9
   |
34 |         self.constructor
   |         ^^^^^^^^^^^^^^^^
   |
note: ...the reference is valid for the lifetime 'a as defined on the impl at 26:10...
  --> src/main.rs:26:10
   |
26 | impl<'x, 'a> SubConstructor<'x, 'a> {
   |          ^^
note: ...but the borrowed content is only valid for the anonymous lifetime #1 defined on the method body at 33:5
  --> src/main.rs:33:5
   |
33 | /     fn leave_sub(&mut self) -> &'a mut Constructor<'x> {
34 | |         self.constructor
35 | |     }
   | |_____^

with the relevant definitions being

struct Constructor<'x> {
    x: &'x usize,
    y: usize
}

struct SubConstructor<'x, 'a> {
    constructor: &'a mut Constructor<'x>
}

impl<'x, 'a> SubConstructor<'x, 'a> {
    fn leave_sub(&mut self) -> &'a mut Constructor<'x> {
        self.constructor
    }
}

These are the semantics you want, so that the borrow returned by leave_sub has the same lifetime as the borrow used to enter the SubBuilder. But, the borrow checker forbids this, and for good reason: with that signature, you would be allowed to call leave_sub twice and obtain aliasing borrows of Constructor!

The conclusion is that leave_sub⁠—and therefore, all methods on SubConstructor⁠—must take self by value.

2 Likes

Thank you for replies, guys!

I tried to play with lifetimes too before opening this thread, but no luck. The reason I would prefer lifetimes instead of pass by value everywhere is that in real implementation the stuff owned by Cunstructor is much more "heavyweight" than presented in these simplified snippets. And I will also have to use these constructors very intensively too. So I concern that moving by value may lead to performance penalties.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.