Builder pattern in RustSergey Potapov October 19, 2021 #rust #patterns
As you know, Rust does not support optional function arguments nor keyword arguments, nor function overloading. To overcome this limitation rust developers frequently apply builder pattern. It requires some extra coding, but from the API ergonomics perspective, gives a similar effect as keyword arguments and optional arguments.
Introduction to problem
Consider the following rust structure:
In Ruby, a class that holds the same data can be defined as:
attr_reader :email, :first_name, :last_name @email = email @first_name = first_name @last_name = last_name end end
Don't worry much about Ruby, I just want you to show how easily a user can be created by explicitly specifying relevant fields:
greyblake = User.new( email: , first_name: , )
last_name is not there, so it gets the default value
Initializing a structure in Rust
Since we do not have default arguments in Rust, in order to initialize such structure we would have to list all fields:
let greyblake = User
This is quite similar to Ruby's keyword arguments, but we have to set all fields although
It works well, but for big complex structures, it can be verbose and annoying.
Alternatively we can implement a
Which will be used in the following way:
let greyblake = new
But it became even worse: we still have to list values for all the fields, but now it's much easier to screw up by passing values in the wrong order (yeah, the newtype technique could help us here, but this article is not about that 🐻).
The Builder pattern to rescue
A builder is an extra structure, that provides an ergonomic interface to set values and a method to build a target structure.
UserBuilder that helps us to build
The things worth noticing:
- A builder resembles a target structure it builds:
UserBuilderhas the same fields as
- There is one setter function per field:
- A setter function consumes a builder(
mut self), sets a value, and returns the builder back. This enables ergonomic chain method calls.
new()creates a builder with predefined defaults (in this case all values are
build()constructs and returns the target structure
- it's not related to builder pattern directly, but we've updated setters to receive
impl Into<String>instead of
String. This makes our API more flexible.
Usually for convenience
User would implement
builder() associated function, so
UserBuilder does not have to be imported explicitly:
Eventually with the builder now we can construct the same user structure:
let greyblake = builder .email .first_name .build;
While it is still slightly more verbose than the Ruby version of
User.new, we got the traits we were aiming for:
- Irrelevant fields are skipped and get default values implicitly
- Relevant fields and their values are clearly spelled out
- No more noise from
Option<T>type, no need for
Now imagine that
User structure has mandatory fields
Builder can not have reasonable defaults for
Again, in Ruby to enforce the presence of
nil values in the constructor:
# ... end end
In Rust to work around the problem, we could adjust the builder's constructor to receive values of mandatory fields:
This allows us to construct a user being sure that
let greyblake = builder .first_name .build;
Unfortunately, it brings us to the same problem which we had with
new() constructor at the beginning of this article:
field names are not spelled out explicitly and it is easy to screw up by passing arguments in the wrong order.
We'll see in the next article how this can be improved with help of Phantom Builder pattern 🐻.
- Discussion on Reddit
- Code on playground - code from this article on Rust Playground.
- derive_builder - Rust macro to automatically implement the builder pattern for arbitrary structs.
- Builder - Builder pattern in "Rust Design Patterns" community book.
Back to top