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.
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.
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.
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:
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.
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.
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).
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).
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
}
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:
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.
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 );`).
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.
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 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:
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:
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.
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.
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?
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.
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).
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.
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.
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...