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 neededThe 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.