Optional Generic Members
Generic type parameters, impl Trait
or const generics used exclusively in optional members break type inference. This problem becomes visible when you skip setting an optional member.
🔴 Bad
#[bon::builder]
fn bad<T: Into<String>>(x1: Option<T>) {
let x1 = x1.map(Into::into);
// ...
}
// This compiles
bad().x1("&str").call();
// This doesn't
bad().call(); // error[E0283]: type annotations needed
The compilation error here is:
bad().call();
^^^ cannot infer type of the type parameter `T` declared on the function `bad`
A similar error would be generated if we used Option<impl Into<String>>
, although the error would reference a generic parameter auto-generated for the function by the builder macro. For simplicity, we'll use a named generic parameter throughout the examples.
The caller of your builder would need to work around this problem by specifying the type T
explicitly via turbofish:
// Both String or `&str` would work as a type hint
bad::<String>().call();
This is inconvenient, don't do this.
🟢 Good
Instead, make the member's type non-generic and move generics to the setter methods' signature. Let's see what it means with an example.
For the case above, the good solution is #[builder(into)]
.
#[bon::builder]
fn good(#[builder(into)] x1: Option<String>) {
// ...
}
good().x1("&str").call();
good().call();
How #[builder(into)]
is different from Option<T>
(T: Into
)? Let's compare the generated code between them (simplified). Switch between the tabs below:
fn good() -> GoodBuilder { /**/ }
impl<S: State> GoodBuilder<S> {
fn x1(self, value: impl Into<String>) -> GoodBuilder<SetX1<S>> {
GoodBuilder { /* other fields */, __x1: value.into() }
}
}
fn bad<T>() -> BadBuilder<T> { /**/ }
impl<T: Into<String>, S: State> BadBuilder<T, S> {
fn x1(self, value: T) -> BadBuilder<T, SetX1<S>> {
BadBuilder { /* other fields */, __x1: value }
}
}
Notice how in the builder(into)
example the starting function good()
doesn't declare any generic parameters, while in the Option<T>
example bad()
does have a generic parameter T
.
Also, in the case of builder(into)
the call to .into()
happens inside of the setter method itself (early). In the case of Option<T>
, the call to .into()
is deferred to the finishing function.
This is also visible when you compare the original functions again. Notice how in the Good
example we already have a concrete Option<String>
type while in the Bad
example, we have to manually call x1.map(Into::into)
:
#[bon::builder]
fn good(#[builder(into)] x1: Option<String>) {
// ...
}
#[bon::builder]
fn bad<T: Into<String>>(x1: Option<T>) {
let x1 = x1.map(Into::into);
// ...
}
So, the builder has to carry the generic parameter T
in its type for you to be able to use that type in your function body to merely invoke Into::into
on a parameter. Instead, strive to do such conversions in setters.
If you are doing a conversion with something other than Into
, then use #[builder(with)]
to apply the same technique for getting rid of generic parameters early.