Crate using

source ·
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 returning Self:

    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 take self, otherwise method chaining does not work (except we require a call to clone or something similar, which is not really intuitive). Therefore, we cannot just move out of self, 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 calling build again would have a different behavior, which, again, is unintuitive.

  • Combining the two approaches above, e.g. by implementing methods xyz and with_xyz, where xyz takes &mut self and with_xyz takes self. 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.