

A const builder pattern in Rust
source link: https://wapl.es/rust/2022/07/03/const-builder-pattern.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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:
struct Client<const N: usize, const D: usize, const TIMEOUT: u64> {
foo: [u8; N],
bar: [u8; D],
idx: u8
}
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 = PrimitiveStyleBuilder::new()
.stroke_width(5)
.stroke_color(Rgb565::RED)
.fill_color(Rgb565::GREEN)
.build();
let radii = CornerRadiiBuilder::new()
.top_left(Size::new(5, 6))
.top_right(Size::new(7, 8))
.bottom_right(Size::new(9, 10))
.bottom_left(Size::new(11, 12))
.build();
RoundedRectangle::new(Rectangle::new(Point::new(5, 5), Size::new(40, 50)), radii)
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 = PrimitiveStyle::new(5, Rgb565::RED, Rgb565::GREEN);
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 = PrimitiveStyle::new(Some(5), Some(Rgb565::RED), None);
Const builders
We need a slightly different pattern with a builder for const
parameters. Let’s see what a first
incarnation looks like.
struct ConstBuilder<const N: usize, const D: usize, const TIMEOUT: u64>;
impl<const N: usize, const D: usize, const TIMEOUT: u64> ConstBuilder<N, D, TIMEOUT> {
const fn with_n<const N_SET: usize>(self) -> ConstBuilder<N_SET, D, TIMEOUT> {
ConstBuilder::<N_SET, D, TIMEOUT>
}
const fn with_d<const D_SET: usize>(self) -> ConstBuilder<N, D_SET, TIMEOUT> {
ConstBuilder::<N, D_SET, TIMEOUT>
}
const fn with_timeout<const TIMEOUT_SET: u64>(self) -> ConstBuilder<N, D, TIMEOUT_SET> {
ConstBuilder::<N, D, TIMEOUT_SET>
}
const fn build(self) -> Client<N, D, TIMEOUT> {
Client {
foo: [0x00; N],
bar: [0x00; D],
idx: 0,
}
}
}
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 = ConstBuilder::new()
.with_n::<16>()
.with_d::<32>()
.with_timeout::<30_000>()
.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 fn new() -> Self {
Self
}
|
61 | let thing = ConstBuilder::new()
| ^^^^^^^^^^^^^^^^^ cannot infer the value of const parameter `N`
Alright then, how about…
const fn new() -> ConstBuilder<N, D, TIMEOUT> {
ConstBuilder::<N, D, TIMEOUT>
}
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:
impl ConstBuilder<16, 16, 30_000> {
const fn new() -> Self {
Self
}
}
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;
impl ConstBuilder<DEFAULT_N, DEFAULT_D, DEFAULT_TIMEOUT> {
const fn new() -> Self {
Self
}
}
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 core::future;
use core::time::Duration;
use tokio::time::error::Elapsed;
const DEFAULT_N: usize = 16;
const DEFAULT_D: usize = 16;
const DEFAULT_TIMEOUT: u64 = 30_000;
struct ConstBuilder<const N: usize, const D: usize, const TIMEOUT: u64>;
impl ConstBuilder<DEFAULT_N, DEFAULT_D, DEFAULT_TIMEOUT> {
const fn new() -> Self {
Self
}
}
impl<const N: usize, const D: usize, const TIMEOUT: u64> ConstBuilder<N, D, TIMEOUT> {
// Compile error: "cannot infer the value of const parameter `N`"
// const fn new() -> ConstBuilder<N, D, TIMEOUT> {
// ConstBuilder::<N, D, TIMEOUT>
// }
const fn with_n<const N_SET: usize>(self) -> ConstBuilder<N_SET, D, TIMEOUT> {
ConstBuilder::<N_SET, D, TIMEOUT>
}
const fn with_d<const D_SET: usize>(self) -> ConstBuilder<N, D_SET, TIMEOUT> {
ConstBuilder::<N, D_SET, TIMEOUT>
}
const fn with_timeout<const TIMEOUT_SET: u64>(self) -> ConstBuilder<N, D, TIMEOUT_SET> {
ConstBuilder::<N, D, TIMEOUT_SET>
}
const fn build(self) -> Client<N, D, TIMEOUT> {
Client {
foo: [0x00; N],
bar: [0x00; D],
idx: 0,
}
}
}
struct Client<const N: usize, const D: usize, const TIMEOUT: u64> {
foo: [u8; N],
bar: [u8; D],
idx: u8,
}
impl<const N: usize, const D: usize, const TIMEOUT: u64> Client<N, D, TIMEOUT> {
async fn do_a_thing(&self) -> Result<u8, Elapsed> {
let fut = future::ready(1u8);
dbg!(N);
dbg!(D);
dbg!(TIMEOUT);
tokio::time::timeout(Duration::from_nanos(TIMEOUT), fut).await
}
}
#[tokio::main]
async fn main() {
let thing = ConstBuilder::new()
// `N` left at its default
// .with_n::<16>()
.with_d::<32>()
.with_timeout::<30_000>()
.build();
thing.do_a_thing().await;
}
Recommend
-
56
DSLs can be an extremely useful way to create a declarative language for a specific use-case in your app. While attending KotlinConf 2018, I witnessed a lot of interest in DSLs. There were several…
-
10
Quick Post: Filter Records in TypeScript using Builder Pattern Today, we revisit the builder pattern and instead of using C#, we're building it in TypeScript Written by Jonathan Danylko • Last Updated: November 6th, 2020...
-
12
Extensible fluent API with builder pattern and extension methods Motivation Having the object in a properly state was a huge and important part of object oriented programming. Meanwhile, it's hard to...
-
10
Using the builder pattern to define test scenarios December 4, 2020 · About 5 minutes · Tags: endbasic,
-
11
Type safe builder pattern September 17, 2019 | 17 Minute Read A powerful type system allows you to write code that is verified by the compiler to be correct to some extent before you ship it into production. I’ve...
-
7
Builder Pattern 在 Objective-C 中的使用 2015-02-07 在说 Builder Pattern 之前,我们先来看看一个场景。假设我们要预定一个 iPhone 6,要 64G 的,金色的,用代码表述大概是这样 // PFX 是...
-
6
Ruby Magic Configurable Ruby Modules: The Module Builder Pattern Michael Kohl on Nov 29, 2019 “I absolutely love AppSignal.” D...
-
12
New issue support pattern as const parents in type_of #80551
-
3
Builder pattern in Rust 2021-10-19 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
-
5
Weekly Rust Trivia: How to Implement the Builder Pattern Published Thu, Jul 6, 2023 / by
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK