Live data from Hacker News

Builder Pattern in Rust

greyblake.com

81–90 of 93 posts

Re: Builder Pattern in Rust

#81

Interesting, i wouldn't reach for a builder pattern in the article's scenario: struct User { email: Option , first_name: Option , last_name: Option } 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.

Re: Builder Pattern in Rust

#82

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.

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

Re: Builder Pattern in Rust

#83
post #2

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.

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 
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) -> 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.)

Re: Builder Pattern in Rust

#84
post #66

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 tab…

> 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.

Re: Builder Pattern in Rust

#85

Earlier quoted context omitted.

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…

> 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.

Re: Builder Pattern in Rust

#86
post #85

Earlier quoted context omitted.

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…

> 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.

Re: Builder Pattern in Rust

#87
post #66

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 tab…

"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.

Re: Builder Pattern in Rust

#88
post #66

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 tab…

> 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.

Re: Builder Pattern in Rust

#89
post #36

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…

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?

Re: Builder Pattern in Rust

#90
post #38

Earlier quoted context omitted.

> 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.
Post reply on HN