Rust: Builder pattern by example
To begin learning Rust in earnest, I recently started writing a client library for Twilio 1. As a library writer, (quoting Alan Kay) you want to make simple things simple but complex things possible. For example, to make an outbound call with Twilio, there are three required parameters but a whole lot of optional parameters that aren’t used very often.
Need for the builder pattern
All outbound call parameters can be representing as a struct that looks like this (I’ve omitted most of the optional parameters for brevity):
This struct has a large number of fields. As such, forcing the application programmer to populate the entire struct
consisting of mostly None
values is unergonomic. Rust does not (yet) have default values for struct fields or default
function arguments, although they have been proposed 2. A nice way to solve this is to use
the builder pattern:
Here, we only accept the mandatory fields in the constructor, and provide methods to optionally fill in the rest of the fields.
As a Rust newbie, there were a couple of subtleties involved in implementing the builder pattern. Let’s go over them.
A first pass
My first stab at a builder implementation looked like this:
Here, each builder method returns self
to allow for chaining, so we can write:
This works! But what if we had more complex builder logic, like:
This fails with a compile error:
error[E0382]: use of moved value: `builder` --> src/main.rs:53:16 | 52 | builder.with_fallback_url("http://www.fallback.com"); | ------- value moved here 53 | let call = builder.build(); | ^^^^^^^ value used here after move
Aha, this is because each builder method is taking ownership of self
and the return statement is relinquishing ownership
back to the caller. There’s only one problem - the return value is not assigned to anything, and hence there is no owner! We can make the
compiler happy by re-assigning to builder each time a builder method is called,
so that the owner is not dropped prematurely:
let mut builder = OutboundCallBuilder::new("tom", "jerry", "http://www.example.com"); if (need_fallback) { builder = builder.with_fallback_url("http://www.fallback.com"); } let call = builder.build();
This is pretty inelegant IMO and again places an unnecessary burden on the library consumer.
Getting it right
To avoid having the builder methods take ownership of self
, we can instead take and return a mutable reference
to self
:
This solves the multi-statement builder problem, but compilation now fails on the one-liner instead:
| 72 | let call = OutboundCallBuilder::new("tom", "jerry", "http://www.example.com") | ________________^ 73 | | .with_fallback_url("http://fallback.com") | |_________________________________________________^ cannot move out of borrowed content
Duh, of course! My build()
method is still consuming (taking ownership) of self
, but we’re passing
it a borrowed reference. Let’s fix that by consuming self
by reference in build()
:
This allows for both multi-line and one-liners! Note, however, that I could do this only because my struct
doesn’t require owned
data. If it did, then build()
would be required to take ownership of self
- in this case, there would
be no option but to sacrifice some usability.
-
Disclaimer: I work at Twilio, but this will be an unofficial library. Also to reiterate, all opinions expressed on this blog are my own and do not reflect the views of my employer. ↩
-
An alternate way is to derive the
Default
trait for the struct, as this stackoverflow answer indicates. However, it still requires the user to pass..Default::default()
which is IMO ugly. ↩