One generic parameter vs multiple parameters

Hi all,

I'm adding more traits to my text wrapping library, and I've run into a situation where my code works with one generic type parameter, but fails to compile with two parameters.

I've recreated the problem in this playground example.

The playground link has the full code, but in short, I have empty traits TraitA and TraitB, and I have structs OptionsA and OptionsAB:

struct OptionsA<A = Box<dyn TraitA>> {
    a: A,
    x: i32,
}

struct OptionsAB<A = Box<dyn TraitA>, B = Box<dyn TraitB>> {
    a: A,
    b: B,
    x: i32,
}

The structs implement a builder pattern and lets you modify the a and b fields. So to set a, the user would call this method:

impl<A> OptionsA<A> {
    fn set_a<NewA>(self, new_a: NewA) -> OptionsA<NewA> {
        OptionsA {
            a: new_a,
            x: self.x,
        }
    }
}

there are analog methods for OptionsAB and for set_b.

Now, my question is: I've noticed that I can use the struct with one generic parameters like this:

let cast_opt_a = OptionsA::new().set_a(Box::new(AAA) as Box<dyn TraitA>);
let defaults_opt_a: OptionsA = OptionsA::new().set_a(Box::new(AAA));

Both values are of type OptionsA<Box<dyn TraitA>>.

When I try to do the same for my two-parameter struct, I get a compilation error:

let cast_opt_ab = OptionsAB::new()
    .set_a(Box::new(AAA) as Box<dyn TraitA>)
    .set_b(Box::new(BBB) as Box<dyn TraitB>);
let dynamic_opt_ab: OptionsAB = OptionsAB::new()
    .set_a(Box::new(AAA))
    .set_b(Box::new(BBB));

I find that the first line works and gives me a value of type OptionsAB<Box<dyn TraitA>, Box<dyn TraitB>> with two trait objects. Howevr, the dynamic_opt_ab line gives me a compilation error:

    |
101 |       let dynamic_opt_ab: OptionsAB = OptionsAB::new()
    |  _________________________---------___^
    | |                         |
    | |                         expected due to this
102 | |         .set_a(Box::new(AAA))
103 | |         .set_b(Box::new(BBB));
    | |_____________________________^ expected trait object `dyn TraitA`, found struct `AAA`
    |
    = note: expected struct `OptionsAB<Box<(dyn TraitA + 'static)>, Box<(dyn TraitB + 'static)>>`
               found struct `OptionsAB<Box<AAA>, Box<BBB>>`

Is there a nice way for me to let users customize the wrapping options in this way without having to specify the as Box<...> for every trait?

One workaround that crossed my mind was to add extra methods for the Box<...> case. However, I would prefer to avoid this if possible.

Thanks for any insights!

(Playground)

I don't have a good answer, but below are a couple more examples I came up with while playing.

You don't have to do it for every trait, just every trait but the last :wink: :

    // `OptionsAB<Box<dyn TraitA>, Box<dyn TraitB>>`
    // as desired
    let dynamic_opt_ab: OptionsAB = OptionsAB::new()
         .set_a(Box::new(AAA) as Box<dyn TraitA>)
         .set_b(Box::new(BBB));
    // So is this one
    let dynamic_opt_ab: OptionsAB = OptionsAB::new()
         .set_b(Box::new(BBB) as Box<dyn TraitB>)
         .set_a(Box::new(AAA));

Honestly, it does seem like quite a mess. A default type will sort of over-ride inference, but for one generic type only. And you can take away that precedence from the default type too:

    // Now this is `OptionsAB<Box<AAA>, Box<dyn TraitB>>`
    // (no `dyn TraitA`)
    let dynamic_opt_ab: OptionsAB<_> = OptionsAB::new()
         //                      ^^^ new
         .set_b(Box::new(BBB) as Box<dyn TraitB>)
         .set_a(Box::new(AAA));

I guess this is expected, but I failed to find any reference on how inferences and defaults play together (or don't play together).

Issue 27336 may be related, not entirely sure. More crumb trails here. Hopefully someone else has more insight.

2 Likes

Hi @quinedot, thank you for looking into this! I'm glad to hear that this corner of the type system is a bit confusing to others too :slight_smile:

I had not noticed that the defaults would kick in for the last parameter — and that it's the last parameter to be set, not the last parameter in the declaration.

Perhaps I could simply give my structs as_dyn methods which would return Box<dyn TraitA> and Box<dyn TraitB>. Something like this (updated on the playground):

impl AAA {
    fn as_dyn(self) -> Box<dyn TraitA> { Box::new(self) }
}

impl BBB {
    fn as_dyn(self) -> Box<dyn TraitB> { Box::new(self) }
}

Then one could do

let options = OptionsAB::new().set_a(AAA.as_dyn()).set_b(BBB.as_dyn());

It could also be an associated method (with no self), but I think I would like to pass data into my AAA and BBB structs, so it seems nice that it can be a method as well.

Another workaround would be to put Box<dyn> methods on OptionAB:

fn set_dyn_a(self, new_a: Box<dyn TraitA>) -> OptionsAB<Box<dyn TraitA>, B> {
    OptionsAB {
        a: new_a,
        b: self.b,
        x: self.x,
    }
}

Or, as it turns out, Rust will find the default if you prompt it to infer with as _, in case that is good enough:

let x: OptionsAB = OptionsAB::new()
    .set_a(Box::new(AAA) as _)
    .set_b(Box::new(BBB) as _); // last one is optional

Playground with both.

1 Like

Thank you very much for helping me explore the design space!

I had not thought of this "short hand", it's pretty neat! :slight_smile: It gives users a fairly clean and simple way of getting dynamic dispatch when they need it.

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.