Generics fun with builders

NOTE: Thanks to everyone! @ 2e71828 solved it in the most elegant way: Rust Playground

Hi! I'm trying to use the builder pattern and struggling to get the generics/trait declarations correct.

Specially, I have a builder which wants to be generic over a particular trait, but that builder is itself optionally embedded in another builder. This means the top level builder cannot know the specifics of the nested generic specialisation (AFAICS).

I don't know whether this is a "don't do this in Rust" or "of course you can - just do it this way.." type issue, so any and all help is appreciated.

As an example (Rust Playground),
Builder wants to optionally accept a ThingOnBuilder trait which should be generic but those types aren't provided for Builder as they may not even be used!

Each configured option on Builder are orthogonal so I want to avoid an explosion of N traits.

Help :slight_smile: Thanks!

/// a "production component" I'm trying to hide behind a builder
mod thing_one {
    pub trait TraitA {}

    pub fn do_stuff<A: TraitA>(a: A) {}
}

/// the builder trait
trait IBuild {
    fn with_logging(self) -> Self;
    fn with_thing_one(self, builder: ThingOneBuilder) -> Self;
}

/// a simple implementation
struct Builder {
    is_needs_logging: bool,

    // a builder for each of the production components...
    is_needs_thing_one: Option<ThingOneBuilder>,
    // is_needs_something_else...
    // is_needs_another_thing...
    // is_needs_yet_another_thing...
}
impl Builder {
    fn new(scope: &str) -> impl IBuild {
        Self
    }
}

/// a builder for one of the production components
trait IBuildThingOne {
    fn with_a<A: thing_one::TraitA>(self, a: A) -> Self;
}
struct ThingOneBuilder {}
impl IBuildThingOne for ThingOneBuilder {
    fn with_a<A: thing_one::TraitA>(self, a: A) -> Self {
        todo!()
    }
}

fn main() {
    {
        let my_concrete_trait_a = SomeStructImplementingThingOneTraitA {};
        Builder::new("my module")
            .with_logging()
            .with_thing_one(ThingOneBuilder {}.with_a(my_concrete_trait_a))
            .build();
    }

    // or

    {
        let builder = Builder::new("my module").with_logging().build();
    }
}

Could you describe the use case more concretely? I'm struggling to even comprehend what you are trying to achieve. The code in the playground doesn't compile due to two very obvious mistakes, which seem to be completely unrelated to your question (a missing type and a missing method on the IBuild trait). So I assume you are trying to fix some other error, but I don't know exactly what.

1 Like

I know it doesn't compile ;-). I'm asking for advice on how to achieve what I'm aiming for. I don't know how to explain what I'm asking for with any more clarity than my original post.

The kernel of my issue is (from OP):
" Builder wants to optionally accept a ThingOnBuilder trait which should be generic but those types aren't provided for Builder as they may not even be used!"

Try not to overthink my request and assuming I'm asking for something else :-).

here's another attempt with the builder stuff built in (I left it out as I thought it was "obvious" and didn't want to clutter the code. Apologies):

Look at ThingOneBuilder. How should that be typed. If it is generic then those generics "leak" out into the outer Builder and that can't possible work (AFAICS).

It sounds like you may want to store the TraitA implementor as a dyn TraitA. E.g.:

struct ThingOneBuilder {
    a: Option<Box<dyn thing_one::TraitA>>,
}
impl IBuildThingOne for ThingOneBuilder {
    fn with_a<A: thing_one::TraitA>(self, a: A) -> Self {
        Self {
            a: Some(Box::new(a)),
        }
    }
}

EDIT: Wrapped in an Option for the case where it's missing.

1 Like

Of course it can work! Your builder is not obliged to return the same builder! It may return different type. And that different type may easily include information about what you added to it.

That sounds suspiciously like XY Problem β€œI want to write some other language in Rust” and not something which someone may actually want.

Complex builders in Rust are easy to write if you use a typestate and then there are no issues with generics at all (everything is generic, but you don't have special traits for builders).

Or, alternatively, make everything dynamic with Box<dyn Trait> and then typechecking wouldn't detect any error, but they may be detected at runtime.

What you are trying to achieve is a weird mix of both and I strongly suspect that some other language does some memory allocations and makes things dynamic without you knowing about them.

1 Like

100% this. I'm getting tripped up with generics and overstating them and getting into trouble.

so it's this simple. Thanks. I was stating more type info than I needed and getting into real trouble. (I'm still tripping up mixing and matching impl T, Box<dyn T>, and &dyn T)

Do you mean "I don't want it"? Because if that's not why you are saying this, then it's not really true – technically, it's perfectly possible to have the builder be generic. Playground.

If you simply don't want it, then you'll need a trait object, as Alice has shown above.

dyn Trait is for type erasure, i.e., it's roughly the run-time, dynamic equivalent of generics. As a heuristic, if you need a function where any generic type (constrained by an appropriate bound) goes in and a concrete, non-generic type comes out, then you want a trait object.

(Whether or not you want to box it is completely orthogonal; it's just the normal question of owning or borrowing, as with any type.)

1 Like

so HOW does that work?!? That's just insane!??! How can Builder be generic on a type it might never see (i.e. in the second use of the builder in main)...

30 years writing software and I Just Don't Get Generics in Rust :-).

(and thanks!)

Hmm, I'm not sure I'm getting what your question is. The whole point of a generic type parameter is that you can substitute it with any concrete type (as long as it upholds the specified bounds of course), and the compiler will instantiate and generate specialized code for the concrete type right there and then.

1 Like

In comes builder with one type, out comes type with another type. Something like this

The problem is that you are start from the wrong end: you are trying to build some kind of perfect builder without clear understanding of what your build is supposed to build in the end.

But the architecture of builder reflects the architecture of what you are trying to build!

You don't start ordering bricks and scaffolding before you would know what you are building in real world (skyscraper would need one approach, bridge would need another), why do you try to do that in your program?

1 Like

In actuality it's not as bad as it appears ;-). I already have the small units, I'm trying to consolidate them into a single Builder.

My false assumption is "making Thing generic requires specialising it when constructing Thing".

Because Builder<A> is generic over A I assumed an A must be provided when instantiating Builder, but that is not the case.

Today is a good day - I'm losing ignorance quickly :slight_smile:

Ah, hang on, I just noticed you did Builder::<SomeStructImplementingThingOneTraitA>::new("my module").with_logging().build();, which isn't what I want as SomeStructImplementingThingOneTraitA is the very thing I want to avoid.

Thanks @khimru - I want to avoid that pattern because I end up with an explosion of N! combinations.

Why? You just create one generic type with N arguments and while in theory it may lead to instantiation of N! types it may only happen if you program really need these N! builders.

1 Like

It is exactly the case. Generic "types" are not types and generic "traits" are not traits. They are type constructors and trait constructors, respectively. Builder, if generic, is not a type unto itself, only Builder<A> is for some type A. Now we are doing type-level arithmetic/logic, so A can be a type variable – it doesn't have to be a concrete type such as i32. You are perfectly allowed to substitute another type variable for it in a generic context. (It is basically an implementation detail that actual machine code will only ever be generated for a fully-instantiated generic.)

Avoid in what context? Surely if you want to usefully use your builder to actually build something, you will have to provide a leaf type at some point.

1 Like

That's the nub of the thing - it's optional.