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. 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) 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).
It has been const since Rust 1.39 (https://doc.rust-lang.org/std/string/struct.String.html#meth...)
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?
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
} 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 ends up being an approximation of named optional arguments, albeit with a somewhat more clunky syntax.
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. 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.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...
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.
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.
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?
It's probably really complicated at the language level if it also has to interact with overloading, subtyping, and generics.
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 );`).
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(),
..
}; #[derive(Default)]
struct Person {
name: String,
email: Option<String>,
}
fn main() {
let person = Person {
name: "John Doe".to_owned(),
..Default::default()
};
}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)
In Rust, you need to come up with a new function for each combination of parameters you want.
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.)