How to provide defaults for generic types in builder pattern

Hi,

I'm trying to reduce the need for builder steps. Currently the user has to provide al mandatory elements (even though in the builder they are optional of course).

let permutate = Permutate::builder()
    .with_genotype(genotype)
    .with_reporter(PermutateReporterNoop::new())
    .build()
    .unwrap();

But I would like the PermutateReporterNoop (which implements the R trait below) to be a default (there are others so it adds up), so you can skip it:

let permutate = Permutate::builder()
    .with_genotype(genotype)
    .build()
    .unwrap();

But I'm not able to do this. What am I not understanding here?
The struct and builder are below (stripped down to essence):

pub struct Permutate<
    G: PermutableGenotype,
    R: PermutateReporter<Genotype = G>,
> {
    genotype: G,
    reporter: R,
}

impl<G: PermutableGenotype, R: PermutateReporter<Genotype = G>>
    TryFrom<PermutateBuilder<G, R>> for Permutate<G, R>
{
    type Error = TryFromPermutateBuilderError;

    fn try_from(builder: PermutateBuilder<G, R>) -> Result<Self, Self::Error> {
        if builder.genotype.is_none() {
            Err(TryFromPermutateBuilderError(
                "Permutate requires a Genotype",
            ))
        } else if builder.reporter.is_none() {
            Err(TryFromPermutateBuilderError(
                "Permutate requires a Reporter",
            ))
        } else {
            Ok(Self {
                genotype: builder.genotype.unwrap(),
                reporter: builder.reporter.unwrap(),
            })
        }
    }
}

pub struct Builder<
    G: PermutableGenotype,
    R: PermutateReporter<Genotype = G>,
> {
    pub genotype: Option<G>,
    pub reporter: Option<R>,
}

impl<G: PermutableGenotype, R: PermutateReporter<Genotype = G>>
    Builder<G, R>
{
    pub fn new() -> Self {
        Self::default()
    }
    pub fn build(self) -> Result<Permutate<G, R>, TryFromBuilderError> {
        self.try_into()
    }
    pub fn with_genotype(mut self, genotype: G) -> Self {
        self.genotype = Some(genotype);
        self
    }
    pub fn with_reporter(mut self, reporter: R) -> Self {
        self.reporter = Some(reporter);
        self
    }
}

impl<G: PermutableGenotype, R: PermutateReporter<Genotype = G>> Default
    for Builder<G, R>
{
    fn default() -> Self {
        Self {
            genotype: None,
            reporter: None,
            // reporter: Some(PermutateReporterNoop::new()),
            // reporter: Some(R::default()),
        }
    }
}

extracted from GitHub - basvanwesting/genetic-algorithm: A genetic algorithm implementation for Rust

You can provide defaults for generic type parameters like this, for example:

pub struct Builder<
    G: PermutableGenotype,
    R: PermutateReporter<Genotype = G> = PermutateReporterNoop,
> {
    pub genotype: Option<G>,
    pub reporter: Option<R>,
}

Haven't checked if that can help you remove the with_reporter call in your snippet.

1 Like

Thanks, I have tried that. But somehow an error with the character below is blocking that approach. It seems it is too early for the SR type to be locked in, because it would change if later overwritten by the user, resulting in 2 types at the same time:

   Compiling genetic_algorithm v0.8.0 (/Users/basvanwesting/sandbox/genetic-algorithm)
error[E0308]: mismatched types
   --> src/strategy/permutate.rs:119:47
    |
115 | impl<G: PermutableGenotype, F: Fitness<Genotype = G>, SR: PermutateReporter<Genotype = G>>
    |                                                       -- expected this type parameter
...
119 |         PermutateBuilder::new().with_reporter(PermutateReporterNoop::new())
    |                                 ------------- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected type parameter `SR`, found `Noop<_>`
    |                                 |
    |                                 arguments to this method are incorrect
    |
    = note: expected type parameter `SR`
                       found struct `permutate::reporter::Noop<_>`
help: the return type of this call is `permutate::reporter::Noop<_>` due to the type of the argument passed
   --> src/strategy/permutate.rs:119:9
    |
119 |         PermutateBuilder::new().with_reporter(PermutateReporterNoop::new())
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^----------------------------^
    |                                               |
    |                                               this argument influences the return type of `with_reporter`
note: method defined here
   --> src/strategy/permutate/builder.rs:56:12
    |
56  |     pub fn with_reporter(mut self, reporter: SR) -> Self {
    |            ^^^^^^^^^^^^^           ------------

For more information about this error, try `rustc --explain E0308`.

I have made a scaffold with the problem. It currently won't compile. But maybe someone can get it to work :slight_smile:

What you need do is ensure the type is constrained at every step.

  • Make the builder construction non-generic — have the default types baked in.
  • Make the builder methods capable of changing the type away from the default.

Modification of your playground code:

pub struct Builder<T: TraitA = ObjectA> {
    pub object: Option<T>
}

impl Builder<ObjectA> { // NON-generic impl!
    pub fn new() -> Self { Self { object: None } }
}
impl<T: TraitA> Builder<T> {
    pub fn build(self) -> Result<Target<T>, TryFromBuilderError> {
        self.try_into()
    }

    pub fn with_object<U: TraitA>(mut self, object: U) -> Builder<U> {
        Builder {
            object: Some(object),
        }
    }
}
3 Likes

Yes, this works, with a small tweak below. Thanks!

impl Builder<ObjectA> { // NON-generic impl!
    pub fn new() -> Self { Self { object: Some(ObjectA { member: 2 }) } }
}

If there is a value from the start, maybe the field no longer needs to be an Option at all?

(And for cases where there is no default type and one must be given, you can fill in an invalid type like () that doesn't satisfy the bounds, rather than having an Option that reiterates the missingness dynamically.)

Yes, that is true. Thanks so much.
Working playground below for later reference:

Another playground, This time a mix-and-mash with an optional provided type and an mandatory provided type next to each other in the same builder.

But I don't understand the approach with the invalid type like ()
Could you elaborate a bit on that approach?

The key idea is that you only need to enforce the types meet the trait bounds when you build(). Everything before that can be in an invalid type-state, and you still get full static checking because build() can't be called unless the final state is valid.

The () type is not special here; it is just the obvious choice for a placeholder type that doesn't implement TraitA.

#[derive(Debug)]
pub struct Builder<OT, MT> {  // note: no trait bounds!
    pub optional_object: OT,
    pub mandatory_object: MT,
}
impl Builder<ObjectA, ()> { // note: not generic!
    pub fn new() -> Self {
        Self {
            optional_object: ObjectA { member: 2 },
            mandatory_object: (),
        }
    }
}
impl<OT, MT> Builder<OT, MT> { // note: no trait bounds!
    pub fn with_optional_object<UT: TraitA>(self, object: UT) -> Builder<UT, MT> {
        Builder {
            optional_object: object,
            mandatory_object: self.mandatory_object,
        }
    }
    pub fn with_mandatory_object<MT2>(self, object: MT2) -> Builder<OT, MT2> {
        Builder {
            optional_object: self.optional_object,
            mandatory_object: object,
        }
    }
}
impl<OT: TraitA, MT: TraitA> Builder<OT, MT> { // only here do we have bounds
    pub fn build(self) -> Result<Target<OT, MT>, TryFromBuilderError> {
        self.try_into()
    }
}

Playground with these changes

6 Likes

Thanks, I understand now. This thread levelled me up quite a bit! Thanks for your generous time!

Am I correct that with the () type approach, it is not feasible return a Builder from the Target?

impl<OT: TraitA, MT: TraitA> Target<OT, MT> {
    pub fn builder() -> Builder<ObjectA, ()> {
        Builder::new()
    }
}
let builder = Target::builder()
    .with_optional_object(a)
    .with_mandatory_object(b);

You really don't care about the Target in any form at this point and you don't want to specify any type yet, you just want to return a Builder. But the types of the throwaway Target cannot be inferred, so it can't compile.

   Compiling playground v0.0.1 (/playground)
error[E0283]: type annotations needed
  --> src/main.rs:85:19
   |
85 |     let builder = Target::builder()
   |                   ^^^^^^^^^^^^^^^ cannot infer type of the type parameter `OT` declared on the struct `Target`
   |
   = note: cannot satisfy `_: TraitA`
   = help: the following types implement trait `TraitA`:
             ObjectA
             ObjectB
note: required by a bound in `Target::<OT, MT>::builder`
  --> src/main.rs:34:10
   |
34 | impl<OT: TraitA, MT: TraitA> Target<OT, MT> {
   |          ^^^^^^ required by this bound in `Target::<OT, MT>::builder`
35 |     pub fn builder() -> Builder<ObjectA, ()> {
   |            ------- required by a bound in this associated function
help: consider specifying the generic arguments
   |
85 |     let builder = Target::<OT, MT>::builder()
   |                         ++++++++++

For more information about this error, try `rustc --explain E0283`.
error: could not compile `playground` (bin "playground") due to 1 previous error

You can put builder() in a non-generic impl, just like we did with Builder::new(), and that would then compile.

But then, if someone wrote a type alias type MyTarget = Target<ObjectA, ObjectB>, they would not be able to call MyTarget::builder() since the type parameters don't match.

There is no perfect solution that is ergonomic in all cases here.

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.