Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Builder Pattern in Rust (greyblake.com)
87 points by todsacerdoti on Oct 20, 2021 | hide | past | favorite | 90 comments


> Since we do not have default arguments in Rust, in order to initialize such structure we would have to list all fields:

I'm surprised the article doesn't mention Default:

https://doc.rust-lang.org/std/default/trait.Default.html

It can be combined with the rest pattern to yield something very similar to the solution the author is after.

https://stackoverflow.com/questions/19650265/is-there-a-fast...


What bugs me a bit about Rust is the lack of default parameters. Even with the Default trait, the caller still has to write a bit of boilerplate ( ..Default::default()).

The caller needs to know that the input struct implements Default.

And you would need one input struct per function (if default values differ).

Are there better ways or is it planned to introduce something like default parameters?


Default parameters, optional parameters, and named parameters kind of form this massive design space where they all sorta kinda influence each other, and so while there hasn't been a formal "yes" or "no" directly from the team about these features, they tend to get caught up in a combination of "there are bigger issues to worry about" along with the combinatorial explosion of truly exploring the design space, at least from what I've observed over the years.


Thanks for the insight, Steve! I'm glad the team thinks about these issues.


> Are there better ways

It's a question of taste, but the builder pattern can be considered a “better way”, because of how clunky the use of Default can be.

You would prefer an API with easier to read, documented builders than the ..Default::default() call.


What I dislike about the builder pattern is that it treats input not as one 'blob' of data and thus makes simple things more complicated.


I have the same feeling. I would love to know if a builder object can be zero cost and if it could actually write assembly code the same way as if we would have passed those arguments manually.


This solution works sometimes, but it's not great if any of the fields don't have a sensible default.


You end up having to do

  struct Parameters {
    non_optional_parameter_1: Foo,
    non_optional_parameter_2: Bar,
    extra_options: ParametersWithDefault,
  }

  #[derive(Default)]
  struct ParametersWithDefault {
    ...
  }
which is kind of annoying but not the end of the world.


It's a lot of small papercuts, but

    use module::{ foo, Parameters, ParametersWithDefault };

    fn bar() {
        foo(Parameters {
           non_optional_parameter_1: Foo,
           non_optional_parameter_2: Bar,
           extra_options: ParametersWithDefault {
               optional_param: Baz,
               ...Default::default(),
           }
        });
    }
is a far cry in ergonomics from:

    use module::foo;

    fn bar() {
        foo(_ {
            non_optional_parameter_1: Foo,
            non_optional_parameter_2: Bar,
            optional_param: Baz,
            ..
        });
    }
Even just having to import the extra structs (and lookup the names) is a massive pain. Especially if you need to change to use a slightly different function.


In which case they should be explicitly provided.


Default only assigns parameters to their default value based on their type (e.g. "0" for u8), it's not an actual implementation of actual default parameters.

More details about this and what else is lacking in Rust compared to Kotlin:

https://medium.com/@cedricbeust/what-rust-could-learn-from-k...


You can just implement the Default trait yourself and set sane defaults right? At least that’s how I use it, implement the default trait manually to easily return a initialized struct.


Yes, I was referring to when you derive the trait, I should have been more specific.

Also, this doesn't help at all for function invocation, which is still very cumbersome in Rust in the absence of default parameters, named parameters, and overloading.


You can derive the Default trait (this is the most popular use case) or implement it yourself where you can have custom default values.


I appreciate the tradeoffs that led to the builder pattern becoming commonplace, but it's probably my least favorite part of Rust. Compared to optional named parameters it just feels clunky, verbose and kind of Java-esque.


IMO, the toy example in the article is too simple to show a good use of the pattern. The builder pattern is great if the builder methods configure complicated invariants and are not just setters. Otherwise, there's nothing wrong with making the struct public, as in

  User { id, email, ..Default::default() }
What's Java-esque is having an irrational fear of using public fields for things that are just data containers, but that's in my experience rare in the Rust community.


This pattern is fairly good but doesn't support required arguments.


I haven't filed an RFC yet, but I have a plan that I'm optimistic can land sometime that would let you write:

  struct User {
    id: Id,
    email: EMail,
    name: String = String::new(),
    age: u16 = 0,
  }
  
  let user = User { id, email, .. }; // valid
  let user = User { id, .. }; // complain about email missing
It would leverage `const` expressions, so it would desugar effectively to the same as if you had written:

  const DEFAULT_USER_NAME: String = String::new();
  const DEFAULT_USER_AGE: u16 = 0;

  struct User {
    id: Id,
    email: EMail,
    name: String,
    age: u16,
  }
  
  let user = User { id, email, name: DEFAULT_USER_NAME, age: DEFAULT_USER_AGE };
The above assumes that String::new will be const at some point (it can be).

If this ever lands, then there will be less need to write Builder traits by hand.

Also, at some point the free-standing `default()` function that calls `Default::default()` will land, making it that much shorter to write (without any new features like I'm proposing).


> assumes that String::new will be const at some point

It has been const since Rust 1.39 (https://doc.rust-lang.org/std/string/struct.String.html#meth...)


This old RFC came up elsewhere in the thread and sounds like a similar idea:

https://github.com/rust-lang/rfcs/pull/1806


I assume it would clone the defaults? Otherwise non-`Copy` types would have to be moved out of their const, which you can't do unless it's a refcell (et al).


OT, but how can a value that lives on the heap be const? And how could ownership of a (non-Copy) const be passed somewhere else?


The trick is that an empty string doesn’t actually own any data on the heap. It’s basically a null pointer, and all string methods check for that in some way. This is a typical optimization trick that many languages implement (empty lists and strings are very common). Defining new to be const just makes this optimization a requirement.


But still- at a language semantics level, values can't have multiple owners, and String couldn't implement Copy just for this one case. So how does this reconcile with the borrow-checker?

Or can consts have multiple owners since they're immutable and have a static lifetime? Is this just the first case of a non-Copy being const, so the question has never come up before?


Ah, I get your question now, I thought you were talking about const functions. Well, const values in Rust are funny things, they're not linked into the executable, they don't have an address or a lifetime. That confused me too for a while. They're purely a shortcut for a value expression, not that different from a #define in C.

Statics, in contrast, have a storage location and a static lifetime.

Compare:

  const CONST_STRING: String = String::new();
  static STATIC_STRING: String = String::new();

  fn main() {
      let x: String = CONST_STRING;  // fine
      let y: String = STATIC_STRING; // error: cannot move out of static item
  }


Oh. I think I didn't realize both existed; I'd only used statics before (and knew about const functions), but didn't know about const expressions.


You can mix-and-match, e.g.

  new(required_arg_1, required_arg_2, Options { foo: "bar", ..Default::default() })
or

  Options { foo: "bar", ..Default::default() }).build(required_arg_1, required_arg_2)


It is useful in embedded. For example, you may see something like [1]:

    let _clocks = rcc
        .cfgr
        .hse(HSEClock::new(25_000_000.Hz(), HSEClockMode::Bypass))
        .sysclk(216_000_000.Hz())
        .hclk(216_000_000.Hz())
        .freeze();
where the freeze function does a lot of calculation to set up the peripheral [2].

[1]: https://github.com/stm32-rs/stm32f7xx-hal/blob/0a0d06d5f63c3...

[2]: https://github.com/stm32-rs/stm32f7xx-hal/blob/0a0d06d5f63c3...


It would be the same for named arguments, yes? Only it would be the constructor doing the set-up work.


I agree. I’ve seen a few libraries (I can’t remember which) instead take the approach of a constructor that takes a single-use struct as an argument. The struct implements Default and all of its members are public. You can then use ..Struct::default() to splice in the default values, and override only the values you want to set.

It ends up being an approximation of named optional arguments, albeit with a somewhat more clunky syntax.


I would quite like to see some syntax sugar around this. If we could elide the type and made `...` equivalent to `..Default::default()` it would be a lot closer to kwargs without having to introduce any new "real" features. So we could write:

     f(arg1, arg2, _ {name: "Hello", address, ...})
instead of

     f(arg1, arg2, MyKwargs {name: "Hello", address, ..Default::default()}
Using a struct for this feels natural as you get forwarding and non-exhaustiveness for adding additional fields for free.


I would like this plus the ability to specify per-field defaults on structs like

    struct Foo {
        bar: String,     // No default
        baz: Option<u32> = None,
    }
I think this would give us 90% of what I would want from first-class named parameters.


I'm looking for the time to write exactly that RFC. The parser, for what is worth, already parses that syntax for error recovery :)

If you're interested, this is the last public conversation I had about these: https://internals.rust-lang.org/t/pre-pre-rfc-syntactic-suga...


Or you could just have named arguments with defaults, which is a highly ergonomic and well-tested solution to this problem.


Yeah, I've seen it called the Init Struct pattern: https://xaeroxe.github.io/init-struct-pattern/


That reminds me of a library where I’ve seen it in use: wgpu. For example https://docs.rs/wgpu/0.11.0/wgpu/struct.TextureViewDescripto...


I totally agree. Named parameters are one of those language features which once you use it, you don't want to go back.

Similar to C-like syntax and ADT's / explicit nullability, I think named parameters are just going to be considered the default obvious choice for programming languages in the next 5 years.


Named parameters in C# make refactoring far less error-prone too.

Consider a method that accepts two int parameters: “DeleteUser(int tenantId, int userId)” - if a call-site was DeleteUser(1,2) and you swapped the parameter arguments you’d be in trouble. Having “DeleteUser(tenantId: 1, userId: 2)” makes code not only safer, but also far more self-documenting.


At the risk of being pedantic, I would use separate types there for the id's to avoid this kind of issue, but in general I agree with your point


There's an interesting commonality here: syntactically, `function(Type1(int), Type2(int))` and `function(type1=int, type2=int)` are almost identical. They differ in that types can help guard against errors elsewhere (within or without that function call), but named arguments let you not care about the order.

I've always wondered: what if languages had no guaranteed argument order, but let you skip explicit naming if all parameters were different types? What kind of idioms would that create? Would it mean more newtypes? Would that be cumbersome?


I'm not aware if this exact idea, but dependency injection frameworks have a similar concept where the parameter order doesn't matter and the framework figures out the right order when it calls functions based on the types.

It's probably really complicated at the language level if it also has to interact with overloading, subtyping, and generics.


It's an interesting thought. Personally I don't mind explicit ordering, since it's just one more way to cross-check correctness. Also I can imagine leaning on the type inference to resolve which argument is which might not be the best for compile times.


What would you do about a slice function? Without the parameter names you won't even see if the second parameter is length of the slice or its right end.


In C# (prior to C# 8.0) without parameter names you would get an ambiguous overload error (and overload resolution is strictly by types, not by names - except for extension-methods). C# 8.0 side-stepped this problem by adding an explicit range operator.

However I feel the best approach is to have first-class language support for parameter-arguments-to-type homomorphisms (e.g. `TArgs<YourMethod> args = new( arg1, arg2, arg3 ); YourMethod( args );`).


Yeah I agree there are examples where types are not enough to disambiguate arguments.


In theory, those two types shouldn’t be ints, but rather separate int wrapping types TenantId and UserId so that they can’t be mixed up at all


Unfortunately hardly any ORMs support that (especially not EF, argh), and writing custom value-types is a painful experience given C# (still) doesn't support strict typedefs nor mixins.


If only structs were inheritable, then you could roll your own `struct Wrapper<T>` with operators for implicit conversion, equality, etc, and then extend it with just the name and type. Wouldn't help with the EF pain point though, and I'm sure it might confuse newcomers initially.


Inheritance shouldn't be abused to make-up for the lack of true mixins, though.


I hope that one day the default struct field values RFC will be reconsidered: https://github.com/rust-lang/rfcs/pull/1806.

This would allow a syntax more familiar to users of JavaScript, where fields with their default value can be omitted:

  struct Person {
    name: String,
    email: Option<String> = None,
  }

  let person = Person {
    name: "John Doe".to_owned(),
    ..
  };


This is probably obvious and it isn't 100% alternative to Javascript syntax, but you can leverage Default trait to come a bit closer:

  #[derive(Default)]
  struct Person {
      name: String,
      email: Option<String>,
  }

  fn main() {
      let person = Person {
          name: "John Doe".to_owned(),
          ..Default::default()
      };
  }


The deal breaker with `..Default::default()` is that it only works for structs without any required fields.

Forgetting a required field just means you get the dummy value from Default, that is, if you even manage to implement Default for the struct at all (the required fields may not have a sensible default value)


It would be really nice if .. could be a shortcut for ..Default::default() which would match the syntax of pattern matching.


It's actually worse than Java in that regard since at least, Java supports overloading.

In Rust, you need to come up with a new function for each combination of parameters you want.


It is very Javaesque in my book; I used it a lot in Java:

    public static class Builder implements StrategyLauncher {
        private final Parameters parameters;
        private final Security.ByDate securityByDate;

        public Builder(
                int orderSize, long maxDollars,
                Security.ByDate sbd) {
            this.securityByDate = sbd;
            req(maxDollars > Price.fromDouble(50));
            req(maxDollars <= Price.fromDouble(1000 * 1000));
            parameters = new Parameters();
            parameters.orderSize = orderSize;
            parameters.maxDollars = maxDollars;
        }

        public Builder(Builder other) {
            securityByDate = other.securityByDate;
            parameters = new Parameters(other.parameters);
        }

        private StrategyGreen build(OrderManager om) {
            Parameters parametersCopy = new Parameters(parameters);
            parametersCopy.sec = securityByDate.get(om.getCurrentTime());
            return new StrategyGreen(om, parametersCopy);
        }
     ...
    }
Here both StrategyGreen.Builder and StrategyGreen.Parameters were static nested classes; Parameters contained the dozen or more parameters for this trading strategy, all of which were public, but the .parameters field of the strategy object was private:

        public Distance profitTarget = Distance.bps(7);
        public Distance trailingCloseDistance = Distance.percentage(15);
        public Duration trailingCloseDelay = Duration.minutes(80);
        public Duration tau = Duration.seconds(1);
That saved me some duplication, but unfortunately even Lombok couldn't save me from writing this kind of bullshit:

        public Builder setStartTime(Moment startTime) {
            parameters.startTime = startTime;
            return this;
        }
In Rust, though, I'd think you could easily define macros for that kind of thing if Builder is what you're into? I'm a super novice at Rust, so maybe I'm overlooking something important here.

Also, in many cases, couldn't you just use struct update syntax instead of using mutability? In this case that's not very appealing; instead of writing

    let greyblake = User::builder("13", "greyblake@example.com")
        .first_name("Sergey")
        .build();
I think you'd end up writing

    let greyblake = User {
        first_name: Some("Sergey".into()),
        ..User::with("13", "greyblake@example.com")
    };
which doesn't look like an improvement to me and maybe is actually worse. It does have the advantage that you don't have to write the builder class, with macros or otherwise. In other cases this kind of thing might be more reasonable:

   let foo = Foo { baz: 8, ..DEFAULTFOO };
Even in cases where struct update syntax isn't a good user experience, maybe struct update syntax would provide a less mutability-centric way to implement the Builder pattern, instead using linearity:

    fn with_bar(self, bar: impl Into<String>) -> Self {
        Self { bar: bar.into(), ..self }
    }
— ⁂ —

Plot twist! In the case of my StrategyGreen above, the actual builder invocation was done from Jython:

    builder = (o.StrategyGreen.builder(5,Price.fromDouble(500*1000), sec)
        .setStartTime(o.Moment.at(japan, 2014,04,01, 12,55))
        .setProfitTarget(o.Distance.bps(8))
        .setTau(o.Duration.minutes(5))
    ...
That kind of combination of static-language implementation scripted in a dynamic language can be a very powerful combination (C and sh, C and elisp, C++ and JS, C++ and Lua), but I feel like it didn't really pay off for us in that project; we would have been better off writing everything in Python, in which case the whole Parameters object would have surely just been a dict. (Of course, Python has optional named function parameters, but in this case we really did want to reify a Builder or Parameters kind of object so that we could create a whole sequence of StrategyGreen objects on different trading days.)

I think it didn't pay off for a few different reasons:

· small project size: we were never more than three people, and when we shut down the project, we had only written about 27000 lines of code, roughly half-and-half split between Blub and Python. Java scales better to larger projects—the well-defined interfaces reduce the amount of code spelunking you have to do—but 27 klocs is well within Python's comfort zone.

· constant factor verbosity: Python isn't an order of magnitude better here than Java (my recent dismaying epiphany: https://news.ycombinator.com/item?id=28660097) but it is better by a factor of, like, four, or something. If those 14000 lines of Blub had instead been 3500 lines of Python, that would have been a significant advantage.

· performance: my main reason for choosing Java was that, in some preliminary simulations, CPython was painfully slow, to the point that I thought it would slow down our strategy refinement feedback loop a lot. But maybe you can see from the above code that I was using Java in a not particularly efficient way: although we did use longs for our prices, we had full-fledged boxed objects for Duration, Moment, Distance, Security, Security.ByDate, and so on. (A thing you can't see is that we ran all the strategies on a single event-driven thread, so we weren't drawing on Java's strength in high-performance concurrency, either.) Fortunately (?), the performance requirements didn't turn out to be as stringent as I thought, so my inefficient use of Java was okay. But this also relates to...

· team composition: I was learning Java on the project and simultaneously trying to teach it to everyone else on the team, so we really didn't make optimal use of the Java ecosystem or Java's advantages. Also, teaching Java turned out to be harder than I expected; nobody else on the team got really comfortable with Java, preferring Python, so anything written in Java became a sort of bottleneck. Partly this may be that I'm just really bad at teaching.

· experimental nature: the advantages of dynamic languages are greater for experimental things where you don't know what you're doing and figure it out as you go along. Though I think automated refactoring narrows the gap somewhat, it doesn't eliminate it. Static languages like Java are less costly when you know what you're doing. But everything in this project was experimental. Which relates to...

· extreme testing: because what we were most interested in was whether our strategies would make or lose money if we ran them in production, not some kind of logical or type-theoretic property, we ran them in simulation before running them in production. Like, a lot. We also had JUnit tests for the kinds of properties you can test with JUnit, but almost all of our code got exercised orders of magnitude harder in simulation than it ever did in production. Which means that the kinds of bugs that static type checking catches were less likely to go uncaught in this project than in most others I've worked on.

· Finally, Java itself is kind of a mediocre language, especially in 02014 when we started the project.

As usual, though, technical issues like language choice weren't crucial to the success or failure of the project; what mattered most was how well we did at prioritizing, collaborating effectively, and responding to those problems. (Maybe you can see, we didn't do well at those.)


Where I've really seen builders be useful is when you have lots and lots of parameters, where most of the time defaults are fine, and you may actually want to pass then around to other parts of the systems, perhaps with different dependencies, who will then further modify them.

An example of this would be configuring a client. Your unit tests may want the client to literally just be a function that returns "OK", your integration tests may want the client dynamically take different parameters depending on what part of the system is being tested (but leave the rest the same), and in production it may need to go through some kind of proxy that your company has.

If you just have one method to construct these things, in each of those many, many different places, you have these huge long constructors being called. It becomes difficult to audit, so mistakes can be made as the requirements evolve, but also, even worse, some chap always comes along and refactors the whole system to optimize for "code reuse", but instead makes a single chokepoint where now one change in one part of the system necessitates breaking every other part of the system. The problem is you don't actually experience that until later down the road, but by that point said chap from before has already been touted as a thought leader in the organization, etc...

Builders are really good in these cases. Especially since you can pass the builder objects along and modify them along the way, and then inject them as dependencies where needed. You can also add special typing if you need to provide certain guard rails, I.e. 'AuthorizedClient' once you've supplied username + password, or whatever.


The thing I really don't like about the builder pattern is that it obscures the possibility space. With named arguments/defult parameters, you know exactly what the fields are you need to consider. With builders, you can wander into an unfamiliar codebase, and you have to read through all the methods on this builder object to understand what's going on.


Does autocomplete (ie, pressing "." and waiting for the IDE) help with that?


Why is this better than a configurable config object that you pass around?

I'm downvoted but I am legitimately curious.


I don't think it's a bad question, and it's not like there's a right or wrong answer to any of it. To me, the reason why in the example I gave a builder is better than a config object is because objects necessitate all their fields to be set upfront.

You could then say, "Well why don't you just have getters and setters for all of those fields?". And it's like, ok sure, we could do that, but what if something else is using that object and we're mutating it?

So then you could say, "well what if the setters return a *copy* of the object, the you can then pass along?" And then it's like, ok, well now we basically have a builder, we're just calling it different things.


> what if something else is using that object and we're mutating it?

You're probably aware that in Rust we have other, better safeguards against that, and you were probably talking about languages like Java and C++ where you don't, but I thought I'd mention it in case someone reading the thread doesn't know that.


Yeah totally, I just meant as a pattern.

Nonetheless, in rust even though you may have the borrow checker to safeguard against the mistake, that still leaves you with now needing to accomplish the thing, in spite of the borrow checker.

Which is where using a builder would be one way to do it. So even rust you still need *something*, it’s just safer and more obvious to see why, because you literally can’t share the reference in two places.


Interesting, i wouldn't reach for a builder pattern in the article's scenario:

    struct User {
      email: Option<String>,
      first_name: Option<String>,
      last_name: Option<String>
    }
My gut feel would have been to drop the Option<> and just have different structs with different combinations of fields as needed. E.g. You could consume an PersonName type with an EmailAddress type to produce a User type if that's what you needed.


At that point, you probably don't care about the name of the struct. You just have a named tuple, where the type of the tuple should encode the names and types of the fields.


In a relevant sense, common occurrence of a pattern reveals a weakness in the language where it appears, because no one has succeeded in capturing it in a library so it doesn't need to be coded again.

Thus, C programs are shot through with hash table implementations, because a generally usable hash table library is not possible in C. Rust has a good one in the standard library, so Rust programs with a custom hash table are vanishingly rare.

Ideally, when a pattern is recognized, the language gets a new fundamental primitive that enables constructing the library. Sometimes that is too hard, and the pattern becomes itself a new core language feature. After that, sometimes an enabling fundamental feature is finally added later, and the frozen pattern becomes redundant.

Thus, each big built-in language feature identifies a failure (at some past time) to capture it in a library for lack of the building blocks that would have been needed. Those might have since appeared, but the built-in is still more idiomatic than using a library.

Each pattern represents an opportunity to strengthen the language with a new primitive that would enable eliminating need for any more instances of the pattern. But there are always new patterns, so always more such opportunities. And, that's how a language evolves.


"Design patterns are bug reports against your programming language." - Peter Norvig

what's wrong with C that you cannot design a general-purpose hash table? Is it that you cannot define a general-purpose function to hash an object and check for equality?


The short answer is that it is not C++. The longer answer can be derived by looking at the list of language features used, in a representative Rust or C++ hash library, that C lacks. We may guess that a few of those are not strictly necessary. Still, the list is quite long.

Ultimately, the answer is that C is not designed for abstraction, expressing things that should occur at compile time, but rather just to map closely to instructions that machines that existed in the 1970s offered. It does that. Its offerings for compile-time behavior amount to, mostly, the preprocessor, which understands nothing at all of language semantics, never mind types.

Oddly, Rust's macro language also doesn't know from types.


> In a relevant sense, common occurrence of a pattern reveals a weakness in the language where it appears, because no one has succeeded in capturing it in a library so it doesn't need to be coded again.

Correct but also a tired point. This is stated in the intro to the design patterns book: these things that we are about to explain are “patterns” because the languages we use can’t express them directly.


It is one thing to observe that a language can't express a pattern, but something of an entirely different order to invent the exact primitives that would enable capturing the pattern and others in its penumbra into libraries.


The end of the article teases a post on the Phantom Builder pattern. With a bit of googling, I found an astonishingly similar looking blog post about the Phantom Builder pattern: https://freemasen.com//blog/phantom-builder/index.html


> yeah, the newtype technique could help us here, but this article is not about that

Well... what kind of reasoning is that? Yes, there is a technique to solve that problem (or part of the problem) perfectly but... you choose not to use it so that you can try to figure out a second, suboptimal way?

I don't know about that


Personally I think if you need a builder, you did something wrong in the first place.

If you have a struct with so many defaultable values, you should break it into smaller pieces. Reasonable struct should not need more than 5 properties, unless it's something like ORM mapping of database table.


I don't know where you pulled that 5 number from but it seems to come out of thin air.

Besides, breaking structures in smaller structures this way can lead to code that's much harder to follow, refactor, and maintain.


It's a nice rule of thumb, but it's not always practical. For instance, something like a vulkan compute pipeline has a ton of parameters just due to the complexity of the domain, and breaking it down into sub-objets to a greater extent than is already done would just add artificial hierarchy.


I don't actually do any Rust programming but am very interested in the language. With that disclaimer, I find it unfortunate that this pattern requires something to be mutable that wouldn't necessarily need to be otherwise.


The nice thing about this pattern wrt mutability is that the mutability is scoped very tightly to the statement, since it's not often that you'd need a builder to live longer than a single statement or at the very most the lifetime of the builder. Although I've had to allow a builder to live longer than a single statement for things in the past, nothing is coming to mind at the moment. Because Rust enforces single ownership of mutable resources, it's not all that bad.


> With that disclaimer, I find it unfortunate that this pattern requires something to be mutable that wouldn't necessarily need to be otherwise.

It does not, you can also have the builder consume itself. But it’s often less convenient.

The consumption also allows for required parameters through the builder pattern: have the “configuration” method return a different type. It's essentially a statically typed state machine.


It doesn't need to be mutable, it's just more efficient. Also the mutability only exists within the builder methods - you can build the struct and assign it to an immutable variable.


Good grief - and I thought Java had bad boilerplate. I feel that if these are the features you require of a language, maybe Rust is not what you should be using? There is a reason the language keeps it simple and writing stuff like this, though convenient once or twice (when coding), goes against why I'd pick Rust (hard to beat performance).


The Ruby counter-example would be even better if it used `Struct` instead of a bare Class: https://ruby-doc.org/core/Struct.html

e.g.

  User = Struct.new(:email, :first_name, :last_name)
or to specify them in any order via kw args:

  User = Struct.new(:email, :first_name, :last_name, keyword_init=true)


Currently this is the only thing slightly bothering me when writing Rust code, everything else is an absolute blast and I'm loving every second of it.

Hopefully an industry wide best-practice will develop soon on how to deal with the problems outlined in the article.

Does anybody know what a Phantom Builder is? The article teases it but doesn't say anything.


> Does anybody know what a Phantom Builder is?

Stab in the dark but I would guess that it's using a phantom type parameter to record the state of the builder with regard to its required parameters. This lets you keep the required parameters out of the builder's own constructor, but make the final `build` function only available if the builder is in the right state -- according to the phantom type.


In my personal projects I usually define macros. They can handle a varying number of arguments, which helps a lot.

Though sometimes Rust macros feel just a bit too strict for me, the way they are checked outright prevents some uses. At the same time they've been really helpful for letting me define a lot of options with a compact syntax.


Thanks for the tip, I will keep this in mind


Nice writeup, but you forgot to mention https://crates.io/crates/typed-builder - for more compiletime goodness!


Just add default / optional / named parameters to the language and be done with it.


I doubht that they have enough Ascii characters left over at this point to do it comfortably.


With trait vs fn / impl it is a pinnacle of naming consistency.

On a serious note I have it in my plans to do some semi serious project in it but every time I look at how I should deal with the lack of some common features makes me drop it. So unless specifically requested by clients of mine I inclined to ignore it for now.


Huh? Python does it without any fancy sigils




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: