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 u8
u16
u32
u64
u128
usize
i8
i16
i32
i64
i128
isize
f32
f64
bool
char
Primitive types aren't qualified for an automatic
Into
conversion for several reasonsFirst, it's because
impl Into
breaks 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 call
The reason for this error is that
rustc
can'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_u16
in 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 typei32
for an integer literal andf64
for a floating point literal. In this casei32
is inferred, which isn't convertible tou32
.Requiring an explicit type suffix in numeric literals would be the opposite of what
bon
tries to achieve with its focus on great ergonomics.The second reason is that it's just conventional not to use
impl Into
for 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 Trait
in 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
name
parameter already uses an explicitInto
conversion. There is no need forbon
to 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
impl
block 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();
bon
avoids 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
Into
conversions is to spare the caller from converting the types at the call site if anInto
conversion exists. There aren't many types that implementInto
conversions to complex type expressions involving references, tuples, arrays, function pointers etc.Anyhow, there is likely a subset of simple type expressions for which
bon
may provide an automaticInto
conversion. 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.