How to write an idiomatic build pattern with chained method calls in Rust?
Asked Answered
A

2

4

Based on the following examples, its possible to write a build-pattern with chained method calls in Rust which either passes by value or by reference (with a lifetime specifier)

A builder pattern in Rust may look something like this:

 ui::Button::new()
    .label("Test")
    .align(Align::Center)
    .build();

When writing idiomatic Rust is there a strong preference for one over another?

Is there some good example of how to write this in Rust?

Aceldama answered 12/1, 2017 at 15:37 Comment(5)
While I'm not a fan of the builder pattern, I'd just follow the simple approach from the Rust book. It's pretty idiomatic.Nepheline
Also not especially a fan, coming Python code that used keyword style, eg: ui.button(label="Test", align='CENTER') style, so looking for something similar in Rust.Aceldama
@ideasman42: Rust has Button { label = "Test", align = "CENTER", .. Default::default() } syntax :)Latishalatitude
Yes, I strongly considered using this style of syntax... (will ask another question on this infact), btw, wouldn't it be: Button { label: "Test", .... ?Aceldama
@MatthieuM. Asked here: stackoverflow.com/questions/41629819Aceldama
L
8

There are actually two trade-offs:

  • should the named setter accept self by value or reference?
  • should the final build method accept self by value or reference?

My recommendation is:

  • mutable reference for the setters
  • value for the build method

This differs slightly from the Builder Pattern presented in the Rust Book which uses a reference in build.


Why passing by mutable reference for the setters?

While a compiler may optimize away the moves caused by a call to fn label(self, &str) -> ButtonBuilder, it is not guaranteed.

On the other hand, the mutable reference way is already optimal so that you need not rely on the optimizer.


Why passing by value for the final build?

For builders only composed of Copy fields, there is no difference between build taking self or &self.

However, as soon as the builder contains non-Copy fields, passing &self to build requires deep-cloning these fields.

On the other hand, passing self by value allows build to move the fields, which avoid unnecessary copies.

If one wishes to re-use the builder, then the builder should implement Clone.

Latishalatitude answered 12/1, 2017 at 16:3 Comment(0)
A
4

I've seen the builder pattern mostly implemented by taking ownership of the Builder when modifying it, and by reference for build(). For example,

#[derive(Debug, Eq, PartialEq)]
struct Foo {
    value: usize,
}

struct FooBuilder {
    foos: usize,
    bars: usize,
}

impl FooBuilder {
    fn new() -> FooBuilder {
        FooBuilder {
            foos: 0,
            bars: 0,
        }
    }
    fn set_foos(mut self, foos: usize) -> FooBuilder {
        self.foos = foos;
        self
    }
    fn set_bars(mut self, bars: usize) -> FooBuilder {
        self.bars = bars;
        self
    }
    fn build(&self) -> Foo {
        Foo {
            value: self.foos + self.bars,
        }
    }
}

fn main() {
    let foo = FooBuilder::new()
        .set_foos(2)
        .set_bars(3)
        .build();
    assert_eq!(foo, Foo { value: 5 });
}

Try on Rust Playground

This makes chaining simple, while allowing reuse of the builder.

Age answered 12/1, 2017 at 15:43 Comment(2)
The book, instead, uses a mutable reference to pass the builder (no lifetime), see Builder Pattern. I expect this is for efficiency.Latishalatitude
Ah, and interestingly... I also disagree (with both the book and you) on passing &self to build. Put a String in FooBuilder, and you'll be forced to clone it in build, even if nobody which to reuse the instance of the builder... that's clearly wasteful.Latishalatitude

© 2022 - 2024 — McMap. All rights reserved.