Builder defined independently of built struct

I'm looking for a crate that makes it easy to declare a builder with fluent setters.

I'm just finding crates like derive_builder that take a struct and derive a builder struct with the same fields.

This is very restrictive, and in fact not very useful. For example, a builder might use only core memory or be partially offline (use disk buffers). This information is defined by a boolean that's in the builder, but not in the built structure.

Can anybody suggest a crate making it easy to create a builder with custom fields?

To me it sounds like you are looking for something quite niche that right of the top of my head doesn't seem to be easily abstractable. Or rather, it doesn't sound like using a crate for creating a complex builder has any real benefit over writing the builder yourself.

1 Like

90% of the code is boilerplate—that's why crate like derive_builder exist. No, it's not niche—a builder embodies the a process to build an object. The process itself can have several parameters that affect the performance of the builder, but are irrelevant for the built object. I think you are considering a builder simply a way to have optional parameters for a constructor in fluent style, but the builder design pattern is much more general than that.

That's not at all what I'm considering. To me it sounds like there's some logic inside your builder that determines how it will build something. Where do you want to put that logic? In some argument of a derive macro? I'd rather have a little boilerplate with a proper builder (that is a struct in your crate, that I, as your user, can inspect and understand quickly), rather than some obscure but less boilerplatey DSL that I have to learn before being able to understand what is going on.

2 Likes

I'm unsure what e.g. fluent setters are, but one thing i've done in the past, to share builder settings across builders is to use a HashMap<std::mem::Discriminant<T>, T> instead of a function/field for each setting.
where T is something like.

enum BuilderSettings {
  Foo,
  Bar,
  Baz
}

It is a little annoying to look up fields, having to have a instance of the structure variant to get a discriminant so you can look up the setting in the hashmap, but it does avoid having to wrap a builder within another builder and keep all the settings in sync, if that helps it might be worth a try.

Edit: With that you can add a single function for all settings add_setting(self, BuilderSetting) -> Self or something to that effect, and keep the ugliness around the HashMap an ugly internal detail that is fairly easy to share across builders without much code... I've preferred this to traditional builders but it is kind of an odd pattern...

Edit: There is an instance of this here (This PR was never merged, we ended up duplicating the builder code instead to making it more of a traditional builder)

Yes, there's a lot of logic inside. It's a builder. :man_shrugging:t2:

But reading your comment I'm thinking that probably what I'm actually looking for is a crate that generates automatically setters in fluent style for a structure. Than I'd have my builder structure, use the setters so set up parameters, and implement separately the building logic.

I haven't found such a crate—any suggestion?

UPDATE: Actually, I found it, and it even states it's useful to write builders.

I still have no idea what does "fluent style" mean. Does that just means method chaining? If so, aren't derive_builder already providing this?

I understand your point, but in Rust "builders" are (most?) commonly used simply as "DSLs" for creating struct instances, with not much internal logic besides maybe parameter validation. Particularly, in OOP languages a builder (like a factory) is often used to abstract out the concrete result type created, whereas in Rust a builder almost always creates an object of a single concrete type.

2 Likes

Well, that's what "fluent" means: Fluent interface - Wikipedia

You want a monoid.
I don't know if using that is worth the hassle (in Rust), but that's the most generic version you will get :wink:

That's just a fancy name for something with an associative binary relation and an identity. Sounds still strange? That just means you can "meaningfully" (associative, the sequence of the parts does matter, but not the order, in which they are combined - ("foo".concat("bar")).concat("baz") == "foo".(concat("bar").concat("baz"))) "add"/"concatenate"/"combine", ... whatever verb best describes what is going on, some partial structs into the final one. And that there must be a "default"/"empty"/... struct that you can combine with any other struct and the result does not change the original one.

Example:

trait FluentBuilderMonoid {
    // for all a: a.chain(mk_default()) == a
    //  for all a: mk_default().chain(a) == a
    fn mk_default() -> Self;
    fn chain(self, other: Self) -> Self;
}

let build_result = mk_default().chain(set_some_field_to_something(foo)).chain(do_some_serious calculation_while_constructing()).chain(set_other_fields_to_something(bar, baz));

You can think of that as combining "partial" builder functions containing the actual logic, while the append's task is to combine the partial structs into the result. Which is trivial to do if there are no overlapping fields but needs some "real" work, if the same fields are set in both operands.

I don't think that makes much sense in Rust. It isn't much fun without being able to define infix functions anyway :slight_smile:

You may ask if you can get away with a semigroup, that is, without an identity? That's mostly user-friendliness, which means that the following does not just yield the "default"

let build_result = mk_default().chain(set_some_field_to_something(foo)).chain(set_other_fields_to_something(bar, baz)).chain(mk_default());