The derive_builder crate (docs) helps you reduce boilerplate when you need setter-methods on your struct, e.g. if you want to implement the builder pattern.
#[derive(Builder)]
is your friend ;-), because it generates all this repetitive code for you. It is an extension for the glorious custom_derive macro.
Features
Here are the current features, as of v0.2.0:
Chaining: All setter calls can be chained, they consume and return
&mut self
.
Extensible: You can still define your own implementation of the struct and define additional methods. Just make sure to name them differently than the fields.
Setter type conversions: Setter methods are generic over the input types – you can supply every argument that implements the
Into
trait for the field type.
Documentation and attributes: Setter methods can easily be documented by simply documenting the corresponding field. Similarly
#[cfg(...)]
and #[allow(...)]
attributes are also applied to the setter methods.
Generic structs? No problemo.
A quick example
Just copy+paste this to give it a try.
#[macro_use] extern crate custom_derive;
#[macro_use] extern crate derive_builder;
custom_derive!{
#[derive(Debug, PartialEq, Default, Clone, Builder)]
struct Lorem<T> {
/// `ipsum` may be any `String` (be creative).
ipsum: String,
/// `dolor` is the estimated amount of work.
dolor: T,
}
}
fn main() {
let x = Lorem::default().ipsum("sit").dolor(42).clone();
assert_eq!(x, Lorem { ipsum: "sit".into(), dolor: 42 });
}
show generated code
#[allow(dead_code)]
impl <T> Lorem<T> {
#[doc = r" `ipsum` may be any `String` (be creative)."]
pub fn ipsum<VALUE: Into<String>>(&mut self, value: VALUE) -> &mut Self {
self.ipsum = value.into();
self
}
#[doc = r" `dolor` is the estimated amount of work."]
pub fn dolor<VALUE: Into<T>>(&mut self, value: VALUE) -> &mut Self {
self.dolor = value.into();
self
}
}
Don't forget to add custom_derive
and derive_builder
to your Cargo.toml
. We recommend to use the latest version. With cargo-edit this is as simple as these two shell-commands:
cargo add derive_builder
cargo add custom_derive
In main()
: The last call of clone()
represents the act of building a new struct when our builder is ready. For the sake of brevity we chose clone and pretend we get something brand new. Of course you can implement your own methods for your struct, for as long as their name does not collide with the field/setter names. In the example above, you could define a method build()
, but not dolor()
.
Design Choices
There are different variants of the builder pattern, which can be implemented in rust - they differ in their argument and return type as follows:
-
&mut self -> &mut self
: mutable non-consuming (recommended) -
mut self -> self
: mutable consuming -
&self -> self
: immutable copies
We chose the mutable non-consuming builder pattern, i.e. all setter methods take and return &mut self
instead of either consuming ownership via mut self
or using an immutable mapping &self -> self
. All patterns have their pros and cons, but the (mutable) non-consuming is the recommended pattern for programming in Rust - as discussed in the explanation of the builder pattern (follow the link above).
Caveats
The recommended variant has a lot of advantages but one minor disadvantage, which can be discussed with this line of code from the example above. You may wonder, if we can get rid of the clone()
somehow:
let x = Lorem::default().ipsum("sit").dolor(42).clone();
If we just omit the clone()
, we would try to bind x
directly to the return value of dolor()
. This value however, is only a reference &mut
to a very short-lived instance of Lorem
which does not survive the semicolon at the end of the line!
The compiler will not let us bind x
to something as short-lived as this. We have these options:
- We can use clone or something else to make a copy of everything we need to survive the semicolon (or rust statement, to be more precise). This is not a performance problem, as we discuss below.
- We can use an alternate syntax and immediately bind the result of
Lorem::default()
to some variable. This extends the lifetime of the value to match the lifetime of the variable. We can then continue working with the variable or references thereof. This would break the chaining-syntax and looks like this:
let mut x = Lorem::default(); x.ipsum("sit").dolor(42);
- Yes, we could switch to another builder pattern variant. As discussed above they all have their pros and cons – so expect disadvantages in other areas. However, it is possible that we add opt-in settings for the other variants in the future, if there is a demand for this.
Reasoning
Why is clone()
only a minor disadvantage? Doesn't it hurt the performance goal of Rust?
- In debug builds –> Yes.
- In release builds –> No.
The compiler is smart enough to optimize this away in release mode – even if the code is distributed over different modules or even crates.
Bottom line: The pain-point of the non-consuming variant kind of compiles away.
If you still have a bad feeling about this, you can either run benchmarks, check the release assembler code, or just avoid clone with the alternate syntax from above.
Feedback
We would like to hear your thoughts and ideas about this new crate. Does it help solve your problem? What do you miss?
Credits
@killercup had the idea at our Cologne Rust last meetup about macros. He also did the first implementation. I contributed with discussion and some additional features.