Into conversions
Problem statement
It's often annoying to have to do a type conversion manually when passing a value to a setter. For example, suppose you have a function that accepts a String. You want to pass a string slice such as "Bon" to that function. However, to do that you need to explicitly call "Bon".to_owned() or "Bon".to_string() to do a &str -> String conversion at the call site. This is inconvenient for APIs that are often invoked with hardcoded string values.
Example:
struct User {
name: String,
}
impl User {
fn new(name: String) -> Self {
Self { name }
}
}
let user = User::new("Bon".to_owned());That .to_owned() call is just boilerplate that we'd like to avoid. A common workaround for this problem is to let the function accept impl Into<String>.
struct User {
name: String,
}
impl User {
fn new(name: String) -> Self {
Self { name }
fn new(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
}
let user = User::new("Bon".to_owned());
let user = User::new("Bon"); This makes it possible for the caller to pass a &str. However, the signature of the function becomes a bit more complex and an into() conversion has to be invoked inside of the function implementation manually. So this approach just shifts the boilerplate from the caller to the callee.
How bon solves this problem
The #[builder] macro automatically adds impl Into in the setter methods and invokes the into() conversion internally.
Example:
use bon::bon;
struct User {
name: String,
}
#[bon]
impl User {
#[builder]
fn new(name: String) -> Self {
Self { name }
}
}
let user = User::builder()
.name("Bon")
.build();This also works when #[builder] is placed on top of a free function or a struct.
Example:
use bon::builder;
#[builder]
struct User {
name: String
}
let user = User::builder()
.name("Bon")
.build();use bon::builder;
#[builder]
fn accept_string(
name: String
) {}
let user = accept_string()
.name("Bon")
.call();We didn't need to add any more attributes for bon to figure out that the setter for name needs to accept impl Into<String>. We also didn't change the signature of new(), so it still accepts a String. This is because bon automatically performs an into() conversion inside of the name() setter method.
Types that qualify for an automatic Into conversion
An automatic Into conversion in setter methods applies only to types that are represented by a simple path (e.g. crate::foo::Bar) or a simple identifier (e.g. Bar, String) with the exception of primitive types.
The following list describes the types that don't qualify for an automatic Into conversion with the explanation of the reason.
Primitive types
Unsigned integers Signed integers Floats Other u8u16u32u64u128usizei8i16i32i64i128isizef32f64boolcharPrimitive types aren't qualified for an automatic
Intoconversion for several reasonsFirst, it's because
impl Intobreaks type inference for numeric literal values. For example, the following code doesn't compile.rustfn half(x: impl Into<u32>) -> u32 { x.into() / 2 } half(10);The compile error is the following (Rust playground link):
loghalf(10); ---- ^^ the trait `std::convert::From<i32>` is not implemented for `u32`, | which is required by `{integer}: std::convert::Into<u32>` | required by a bound introduced by this callThe reason for this error is that
rustccan't infer the type for the number literal10, because it could be one of the following types:u8,u16,u32, which all implementInto<u32>. There isn't a suffix like10_u16in this code to tell the compiler the type of the number literal10. When compiler can't infer the type of a numeric literal if falls back to assigning the typei32for an integer literal andf64for a floating point literal. In this casei32is inferred, which isn't convertible tou32.Requiring an explicit type suffix in numeric literals would be the opposite of what
bontries to achieve with its focus on great ergonomics.The second reason is that it's just conventional not to use
impl Intofor primitive types in Rust. There aren't many types that implementInto<bool>orInto<char>, for example. It also keeps simple things (primitive values) simple in the method signatures of the generated builder. Hints in IDE become easier to read.impl Traitin function parameter typesThe reason is that it leads to unnecessarily nested generics that may block type inference.
Example:
rustuse bon::builder; #[builder] fn greet(name: impl Into<String>) { let name = name.into(); println!("Hello {name}") } greet().name("Bon").call();In this case the
nameparameter already uses an explicitIntoconversion. There is no need forbonto add animpl Into<impl Into<String>>conversion on top of that because it would complicate type inference forrustc.The compiler would need to infer two generic types in this case for each of the
impl Into, which it may not always do automatically and it would require providing type hints manually. That would break ergonomics promised bybon.Generic types from the function signature, surrounding
implblock or struct's declaration.The reason is similar to the previous item.
Example:
rustuse bon::builder; #[builder] fn greet<T: Into<String>>(name: T) { let mut name = name.into(); println!("Hello {name}") } greet().name("Bon").call();bonavoids a nestedimpl Into<T>whereT: Into<String>to prevent type inference from stalling.Tuples, arrays, references, function pointers and other type expressions.
Examples:
&str,&mut String,[u8; 10],(u32, u32),&dyn Trait.The reason is that analysis of complex type expressions is also complex.
The goal of the automatic
Intoconversions is to spare the caller from converting the types at the call site if anIntoconversion exists. There aren't many types that implementIntoconversions to complex type expressions involving references, tuples, arrays, function pointers etc.Anyhow, there is likely a subset of simple type expressions for which
bonmay provide an automaticIntoconversion. If you have a use case that needs such conversions to be automatic, you may override the default behavior and consider to open an issue.
Override the default behavior
Suppose automatic Into conversion qualification rules don't satisfy your use case. For example, you want the setter method to accept an Into<(u32, u32)> then you can use an explicit #[builder(into)] to override the default behavior.
Use #[builder(into = false)] if you want to disable the automatic into conversion.
See this attribute's docs for details.