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 consts 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