bon
is a Rust crate for generating compile-time-checked builders for functions and structs.
If you don't know about bon
, then see the motivational blog post and the crate overview.
New features
Improved compile times of the generated code
This release features an improved compilation time of the code generated by bon
. This was tested on a real use case of the frankenstein
crate (Telegram bot API client for Rust) which has ~320 structs with #[builder]
annotations.
If you wonder how this was achieved, then read the section 'How did we improve compile times' with the details.
NOTE
This optimization covers only the code produced by expanding the #[bon::builder]
macro. The bon
crate itself compiles fast because it's just a proc macro.
Better error messages
The #[builder]
macro now benefits from the #[diagnostic::on_unimplemented]
attribute. It now generates a readable compile error with additional context for debugging in the following cases:
- Forgetting to set a required member.
- Setting the same member twice (unintentional overwrite).
Let's see this in action in the following example of code:
#[bon::builder]
struct Point3D {
x: f64,
y: f64,
z: f64,
}
fn main() {
// Not all members are set
let _ = Point3D::builder()
.x(1.0)
.build();
let _ = Point3D::builder()
.x(2.0)
.y(3.0)
.x(4.0) // <--- Oops, `x` was set the second time instead of `z`
.build();
}
When we compile this code, the following errors are generated (truncated the help
and note
noise messages):
error[E0277]: can't finish building yet
--> crates/sandbox/src/main.rs:12:10
|
12 | .build();
| ^^^^^ the member `Point3DBuilder__y` was not set
{...}
error[E0277]: can't finish building yet
--> crates/sandbox/src/main.rs:12:10
|
12 | .build();
| ^^^^^ the member `Point3DBuilder__z` was not set
{...}
error[E0277]: can't set the same member twice
--> crates/sandbox/src/main.rs:17:10
|
17 | .x(4.0) // <--- Oops, `x` was set the second time instead of `z`
| ^ this member was already set
{...}
error[E0277]: can't finish building yet
--> crates/sandbox/src/main.rs:18:10
|
18 | .build();
| ^^^^^ the member `Point3DBuilder__z` was not set
{...}
The previous version of bon
already generated compile errors in these cases, but those errors were the default errors generated by the compiler saying that some internal trait of bon
was not implemented without mentioning the names of the members and without a targeted error message.
INFO
It became possible after adopting the new design for the generated code (see optimizing compile times). This is because the previous design used where
bounds on associated types, for which #[diagnostic::on_unimplemented]
just didn't work.
More #[must_use]
We added #[must_use]
to the build()
method for struct builders. Now this produces a warning because the result of #[build]
is not used:
#[bon::builder]
struct Point {
x: u32
}
Point::builder().x(1).build();
The compiler shows this message:
warning: unused return value of `PointBuilder::<(__X,)>::build` that must be used
--> crates/sandbox/src/main.rs:7:5
|
7 | Point::builder().x(1).build();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: building a struct without using it is likely a bug
bon
now also forwards #[must_use]
from the original function or method to the finishing builder's call()
method:
#[bon::builder]
#[must_use = "Don't forget to star your favourite crates 😺"]
fn important() -> &'static str {
"Important! Didn't you give `bon` a ⭐ on Github?"
}
important().call();
The warning is the following:
warning: unused return value of `ImportantBuilder::call` that must be used
--> crates/sandbox/src/main.rs:8:5
|
8 | important().call();
| ^^^^^^^^^^^^^^^^^^
|
= note: Don't forget to star your favourite crates 😺
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
8 | let _ = important().call();
TIP
Btw, if you are using bon
or you just like its idea and implementation consider giving it a ⭐ on Github.
Big thanks to @EdJoPaTo for implementing this (#82)!
How did we improve compile times
I was integrating bon
into the crate frankenstein
. This is a Telegram bot API client for Rust. It has ~320 big structs with #[builder]
annotations. For example, the Message
struct has 84 fields 🗿.
This project was originally using typed-builder
, but one of the contributors suggested migrating to bon
. I noticed this suggestion and offered my help with the migration because it helps me in identifying the usage patterns for bon
, and if there are any problems with its current design, or if it has bugs or lacks features.
When I worked on this migration I noticed a slowdown in the compilation speed of frankenstein
. The difference was about 5 seconds of the compile time. I got frustrated for a moment, but then I started researching. Thanks to the rustc
self-profiler and cargo build --timings
I quickly detected the bottleneck.
The problem was that type checking of the code generated by bon
was much slower than it was for typed-builder
. The main thing, that was hard to compile for rustc
were the generic associated type references. Here is an example of how the code generated by bon
previously looked like (simplified):
// Stores the type state of the builder
trait BuilderState {
type Member1;
type Member2;
}
impl<Member1, Member2> BuilderState for (Member1, Member2) {
type Member1 = Member1;
type Member2 = Member2;
}
type InitialState = (::bon::private::Unset<String>, ::bon::private::Unset<String>);
struct Builder<State: BuilderState = InitialState> {
member1: State::Member1,
member2: State::Member2,
}
// Setter implementations (just one setter is shown)
impl<State: BuilderState<Member1 = ::bon::private::Unset<String>>> Builder<State> {
fn member1(self, value: String) -> Builder<(
::bon::private::Set<String>,
State::Member2
)> {
Builder {
// Change the type state of `Member` to be `Set`
member1: ::bon::private::Set(value),
// Forward all other members unchanged
member2: self.member2,
}
}
}
// ... other `impl` blocks (one impl block per member)
The associated type references to State::MemberN
are hard for the compiler to resolve. It spends much more time compiling the code when you use them. Not only that but the old version of bon
was generating a separate impl
block for every member. This increased the load on the compiler even more.
The new version of the generated code doesn't use associated types and instead uses separate generic parameters. It also generates a single impl
block for all builder methods (including setters and the final build()
or call()
methods).
The generated code now looks like this (simplified):
type InitialState = (::bon::private::Unset, ::bon::private::Unset);
struct Builder<State = InitialState> {
members: State
}
// Single impl block for all setters
impl<Member1, Member2> Builder<(Member1, Member2)> {
fn member1(self, value: String) -> Builder<(
::bon::private::Set<String>,
Member2
)>
where
Member1: bon::private::IsUnset
{
// Body is irrelevant
}
// ...other setter methods in the same `impl` block here
}
After doing this change, the compilation time of frankenstein
crate decreased by 36%
returning to the level of typed-builder
or even better.
This change also improved the rustdoc
output a bit. Now that there is a single impl
block for the builder, there is less noise in the documentation. Also, this change unlocked the possibility to improve compile error messages.
I am very satisfied with this change and I hope you find your code compiling faster if you are using bon
extensively 🐱.
Summary
We are continuing to improve bon
and add new features. It's still a young crate and we have big plans for it, so stay tuned!
Also, a huge thank you for 600 stars ⭐ on Github! Consider giving bon
a star if you haven't already. Your support and feedback is a big motivation and together we can build a better builder 🐱!