A const builder pattern in Rust
During the creation of EtherCrab, a pure-Rust EtherCAT master, one of the core structs in the crate started growing quite a few const generic parameters. Here's a reduced example of what I'm talking about:
There are/will be a few more parameters in the future, but this is already pretty unwieldy, so let's fix that.
But first: a normal builder
Skip this section if you already know what the builder pattern is :)
For structs with many different parameters, you'll often see the builder pattern used in Rust
crates, for example in embedded-graphics
:
let style = new
.stroke_width
.stroke_color
.fill_color
.build;
let radii = new
.top_left
.top_right
.bottom_right
.bottom_left
.build;
new
The builder allows the final struct (RoundedRectangle
in this case) to have private fields, but
more importantly helps disambiguate passing random values for various fields. Let's take a look at
what creating a PrimitiveStyle
, created by PrimitiveStyleBuilder
in the example above, would
look like without a builder:
// Ordered as above: stroke width, stroke colour, fill colour
let style = new;
Now, assuming we don't have nice inlay hints in our editor showing the argument names, how do we know what the three magic arguments correspond to? We don't! We can take a guess, but that's a recipe for disaster. Hopefully this example demonstrates the added safety and readability that builders provide.
A fantastic extra feature also falls out of the builder pattern: we can have sensible defaults within the builder, meaning the programmer doesn't have to specify all field values every time. In contrast, to support this in the non-builder API it sucks even more:
// Use the default fill colour but override everything else
let style = new;
Const builders
We need a slightly different pattern with a builder for const
parameters. Let's see what a first
incarnation looks like.
;
If your first thought is "wow, that's quite verbose with all the const
s there!" you are absolutely
correct and I agree with you. But the usage isn't so bad:
let thing = new
.
.
.
.build;
That almost looks like a normal builder!
Defaults
But where does new()
come from? This took me a few tries to figure out. Here's the first solution
I reached for:
const
Ah...
|
61 | let thing = ConstBuilder::new()
| ^^^^^^^^^^^^^^^^^ cannot infer the value of const parameter `N`
Alright then, how about...
const
nah, same error. Note that the error also percolates to D
, then TIMEOUT
if we define N
.
We need two things out of this new()
method;
- No errors please
- An ability to initialise the builder with some default values
The solution to both these points is thankfully pretty simple: We must define another impl
block
but this time, we'll use concrete values:
This works, but I admit it does replicate the magic values issue we had with the
let style = PrimitiveStyle::new(5, Rgb565::RED, Rgb565::GREEN);
-style API above. That said, the
defaults are more likely to be contained within the crate or module, so they're not exposed to the
user to make mistakes with - only you, great author, can mess your crate up ;).
That said, we can guard against this a little bit better by giving some names to the default values:
const DEFAULT_N: usize = 16;
const DEFAULT_D: usize = 16;
const DEFAULT_TIMEOUT: u64 = 30_000;
This doesn't prevent reordering defaults of the same type, but perhaps it goes a little way to making the code less error prone.
The whole lot
Overall, I'm pretty happy with this builder pattern. I doubt I'm the first to discover it, but it was a bit of a eureka moment for me and I thought it interesting enough to share. The full code is below, or you can visit the Rust playground to run it yourself.
use future;
use Duration;
use Elapsed;
const DEFAULT_N: usize = 16;
const DEFAULT_D: usize = 16;
const DEFAULT_TIMEOUT: u64 = 30_000;
;
async