7 ms·
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 pa
by dralley 5y ago
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.
- paulgb 5y agoI 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.
- wmanley 5y agoI 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.
- nicoburns 5y agoI 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.
- estebank 5y agoI'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-sugar-for-default-default/13234 https://internals.rust-lang.org/t/pre-pre-rfc-syntactic-suga...
- skohan 5y agoOr you could just have named arguments with defaults, which is a highly ergonomic and well-tested solution to this problem.
- seritools 5y agoYeah, I've seen it called the Init Struct pattern: https://xaeroxe.github.io/init-struct-pattern/ https://xaeroxe.github.io/init-struct-pattern/
- paulgb 5y agoThat reminds me of a library where I’ve seen it in use: wgpu. For example https://docs.rs/wgpu/0.11.0/wgpu/struct.TextureViewDescriptor.html https://docs.rs/wgpu/0.11.0/wgpu/struct.TextureViewDescripto...
- nitsky 5y agoI hope that one day the default struct field values RFC will be reconsidered: https://github.com/rust-lang/rfcs/pull/1806 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(), .. };
- madiyar 5y agoThis 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() }; }
- kevincox 5y agoIt would be really nice if .. could be a shortcut for ..Default::default() which would match the syntax of pattern matching.
- kangalioo 5y agoThe 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)
- codeflo 5y agoIMO, 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.
- kevincox 5y agoThis pattern is fairly good but doesn't support required arguments.
- seritools 5y agoYou 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)
- estebank 5y agoI 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).
- ahupp 5y ago
- pca006132 5y agoIt 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/0a0d06d5f63c3a1471362370fdfb8c367207d308/examples/can-echo.rs#L28-L33 https://github.com/stm32-rs/stm32f7xx-hal/blob/0a0d06d5f63c3... [2]: https://github.com/stm32-rs/stm32f7xx-hal/blob/0a0d06d5f63c3a1471362370fdfb8c367207d308/src/rcc.rs#L746 https://github.com/stm32-rs/stm32f7xx-hal/blob/0a0d06d5f63c3...
- dralley 5y agoIt would be the same for named arguments, yes? Only it would be the constructor doing the set-up work.
- skohan 5y agoI 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.
- DaiPlusPlus 5y agoNamed 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.
- skohan 5y agoAt 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
- lostmsu 5y agoWhat 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.
- DaiPlusPlus 5y agoIn 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 );`).
- skohan 5y ago
- hota_mazi 5y agoIt'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.
- kragen 5y agoIt 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 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.)