Announcing `#[derive(Builder)]`, which generates setter methods for your struct :-)

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.

:ballot_box_with_check: Features

Here are the current features, as of v0.2.0:

:heavy_check_mark: Chaining: All setter calls can be chained, they consume and return &mut self.
:heavy_check_mark: 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.
:heavy_check_mark: 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.
:heavy_check_mark: 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.
:heavy_check_mark: Generic structs? No problemo.

:scissors: A quick example

Just copy+paste this to give it a try. :slight_smile:

#[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().

:interrobang: 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:

  1. &mut self -> &mut self: mutable non-consuming (recommended)
  2. mut self -> self: mutable consuming
  3. &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:

  1. 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.
  2. 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);
  3. 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. :wink:

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.

:mega: Feedback

We would like to hear your thoughts and ideas about this new crate. Does it help solve your problem? What do you miss?

:clap: 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.

23 Likes

This looks really awesome! I just posted a comment to one of the PRs for imag to use this crate! Thanks for reducing my boilerplate code! :smile:

1 Like

This is fantastic! I've been wanting something like this. I like the discussion of the clone(). I ran into this exact problem, and have been hemming and hawing on the right way to deal with it. I didn't know that clone() get's optimized out. Is this documented somewhere? And/Or will it always be true?

typo: 3. &self -> ... should be self edit: there is no typo here, it was my misunderstanding

You are sceptical, that's good. :slight_smile:

Indeed this is a tricky question and an analytical answer would require in-depth knowledge of the compiler and llvm-backend.

This is most likely a counter-example: Let's say you wrap the clone into a function and manually flag this as #[inline(never)] and then use this instead. I would be very much surprised if it compiled away. But this is an extreme case and you basically told the compiler "don't touch this".

The truth is this:

  • We can't promise you, that clone will compile away under all circumstances.
  • Except for #[inline(never)]. We tried really hard to confuse the compiler by distributing code fragments over different modules and even three different crates.

In all our tests the clone was optimized away. We are confident enough to believe that this is not a problem for your every-day rust code.

But we also appreciate a good portion of scepticism. If anyone can find an edge-case which does not get optimized - please share it with us. If this can not be fixed in the rust compiler we will consider changing our documentation and/or options.

1 Like

PS: I can't see a typo in variant 3: "&self -> self: immutable copies". This variant would rely heavily on cloning because it only requires immutable references. Every setter method would return a clone in this variant. The compiler would be very busy optimizing all of this away. :wink:

Oh, I'm sorry... I misunderstood what you were doing there. I was thinking that you meant move, and of course you wrote copies. Your number 2 is more inline with what I was thinking. The mutable move. (so no typo)

But, can you go into why you dislike the consuming variant? I've used that pattern, and I've started favoring it for builders. Is this more to make sure that you have non-consuming options for standard setters?

I've literally ended up with impls where I have all three variants, but I think that's partly because I've always assumed that clone() would be expensive.

.. I did and thought the same thing, before @killercup told me that most clones get optimized. :wink:

Yes, you are a bit more flexible to use mutable non-consuming setters, because you obviously don't need to own it and you don't need to re-assign the return value to a new variable, which can be a convenience thing.

The latter benefit is explained in the yet unofficial rust guidelines (WIP). Actually they miss the third variant right now, but it's still work in progress.

2 Likes

you guys are awesome!

I just wondered why this auto setter and getter deriving is not part of the standard library. :no_mouth:

1 Like

Erm... what's the point if this, when have functional record update?

1 Like

Thx for your critical question. If I understand you correctly, you are suggesting to write something like

build( Lorem { ipsum: "sit", dolor: 42, ..Lorem::default() } );

instead of

Lorem::default().ipsum("sit").dolor(42).build();

It probably depends ..

  • .. if you also add custom setters into the mix of the second approach, you'll have some additional flexibility.
  • .. if you only use the generic implementation, then it's mostly a matter of taste, I guess. Both will work. :slight_smile:

One interesting fact is that inline(never) is not an optimization blocker -- the compiler will still optimize the contents of that function based on information on how it is used (for example if it's always called with the same constant argument).

I'd be skeptical of clones of Strings being optimized out, but it would be cool to see if it does. Of plain scalar values I understand that it can be easily compiled to a no-op.