Expand description
This crate provides the using
macro designed to simplify writing and using builders by
providing method cascading:
#[derive(Debug, Copy, Clone)]
struct Vec3 {
x: f32,
y: f32,
z: f32,
}
#[derive(Default, Debug, Copy, Clone)]
struct Vec3Builder {
x: Option<f32>,
y: Option<f32>,
z: Option<f32>,
}
impl Vec3Builder {
pub fn x(&mut self, x: f32) {
self.x = Some(x);
}
pub fn y(&mut self, y: f32) {
self.y = Some(y);
}
pub fn z(&mut self, z: f32) {
self.z = Some(z);
}
//this also works with `self` instead of `&mut self`
pub fn build(&mut self) -> Vec3 {
Vec3 {
x: self.x.unwrap(),
y: self.y.unwrap(),
z: self.z.unwrap(),
}
}
}
let vec3 = using!(Vec3Builder::default() => {
.x(4.27);
.y(9.71);
.z(13.37);
.build()
});
// Generated code:
//
// let vec3 = {
// let mut target = Vec3Builder::default();
// target.x(4.27);
// target.y(9.71);
// target.z(13.37);
// target.build()
// };
The idea is that instead of implementing builders as fluid interfaces that allow method
chaining (i.e. each method returning &mut Self
or Self
), we implement our builder with
simple setter methods and use it with the using
macro, which gives us the ergonomics of
conventional builders without having to implement the builder as a fluid interface.
The macro is not bound to builders: it has no requirements on the type, therefore we can use it on basically anything:
let map = using!(HashMap::new() => {
.insert("a", 41);
.insert("b", 971);
});
let hello_world = using!(Vec::new() => {
.push("Hello");
.push("World!");
.join(", ")
});
assert_eq!(hello_world, "Hello, World!");
§Motivation
The idea for this crate came from implementing the builder pattern in a personal project. In Rust, there are three main approaches for designing builder structs:
-
All methods taking
self
and returningSelf
:ⓘimpl SomeBuilder { pub fn new() -> Self { ... } pub fn x(self, arg: T) -> Self { ... } pub fn y(self, arg: U) -> Self { ... } pub fn z(self, arg: V) -> Self { ... } pub fn build(self) -> Something { ... } }
The advantage of this method is that when building the final object, the fields can be moved out of the builder. One disadvantage of this method is that using the builder in more complicated ways can become quite verbose: if a method must be called inside an
if
statement or a loop or if the builder must be passed to a function, the builder has to be stored in a mutable variable and re-assigned everytime:ⓘlet mut builder = SomeBuilder::new() .x(...) .y(...); if some_condition { builder = builder.z(...); } if some_other_condition { builder = some_function(builder); } let thing = builder.build();
Also, the builder methods are quite verbose since they have to return
self
. -
All methods taking
&mut self
and returning&mut Self
:ⓘimpl SomeBuilder { pub fn new() -> Self { ... } pub fn x(&mut self, arg: T) -> &mut Self { ... } pub fn y(&mut self, arg: U) -> &mut Self { ... } pub fn z(&mut self, arg: V) -> &mut Self { ... } pub fn build(&mut self) -> Something { ... } }
This improves the disadvantage of the first method with respect to more complicated use-cases, because there are no re-assignments:
ⓘlet mut builder = SomeBuilder::new() .x(...) .y(...); if some_condition { builder.z(...); } if some_other_condition { some_function(&mut builder); } let thing = builder.build();
However, with this method, the
build
method cannot takeself
, otherwise method chaining does not work (except we require a call toclone
or something similar, which is not really intuitive). Therefore, we cannot just move out ofself
, so we might end up in situations where we have to clone objects to be put into the final objects or we have to move out of the builder and leave the builder in a state where callingbuild
again would have a different behavior, which, again, is unintuitive. -
Combining the two approaches above, e.g. by implementing methods
xyz
andwith_xyz
, wherexyz
takes&mut self
andwith_xyz
takesself
. This combines the advantages of both methods, but it makes defining the builder even more verbose and also requires at least one of the two methods for each field to have a longer name.
A problem shared amongst all the approaches above is that having conditionals or loops around calls to the builder break method chaining.
The idea of this crate comes from the observation that the main reason builders are usually designed as fluid interfaces is that we want to express the pattern “here is an object and I want to call these methods on it” without explicitly defining the variable or referencing it everytime. Therefore, we introduce a hypothetical language construct that does exactly that:
let thing = using builder @ SomeBuilder::new() {
x(...);
y(...);
if some_condition {
z(...);
}
if some_other_condition {
some_function(&mut builder);
}
build()
};
This hypothetical using
expression takes an expression of any type (with an optional
@-binding) and a block expression. Inside that block, every public method and every public
field of that type is in the local scope of that block. With that, the example above would be
equivalent to:
let thing = {
let mut builder = SomeBuilder::new();
builder.x(...);
builder.y(...);
if some_condition {
builder.z(...);
}
if some_other_condition {
some_function(&mut builder);
}
builder.build()
};
This is also known as Method cascading and is
an actual feature in some languages, notably Pascal and Visual Basic (initiated with the
keyword with
; we only chose using
because the crate name was free ¯\_(ツ)_/¯).
The using
macro emulates this behavior, with some restrictions due to the way macros are
interpreted, e.g. in the context of macros, we do not know the type of the given expression and
its public symbols, therefore we have to prefix method calls with a dot. Also, this way of
accessing members does not work in all contexts; for details, see the documentation of
using
.
Writing builders with the using
macro can be done by just defining a simple setter method
for each field, making the code for builder very concise. If the to-be-constructed struct is
simple enough, this could even make defining a builder obsolete. Also, the build
method can
now take both self
or &mut self
without breaking method chaining, which is usually a
drawback of defining builders taking &mut self
.
Macros§
- A macro that provides method cascading for an object.