Improving struct init shorthand

What's preventing us from giving more flexibility to the shorthand?

struct Foo {
    n: i32,
    s: String,
}

let n = 42u16;
let s = "bar";

Foo {
    n as i32,
    s.into(),
}

Edit: So I'm proposing using variable names as a struct field names, as we have in the current syntax Foo { n, s }, with additional as and dot as delimiters.

1 Like

Structs aren't ordered for one.

2 Likes

Ah, so it's not so obvious as I expected it to be. I didn't imply any order there, n and s is still there as field names in Foo { n, s }.

Why would the variable names have any relation to the struct field names? That seems way too magical. What if you had something like n + s? Now you're unable to infer the field.

8 Likes

Isn't they already related to the field names in the current version of the shorthand?

1 Like

That's only if you're using the variable name alone. Anything beyond the absolute trivial n: n is not allowed, imo for good reason. As I said, what should n + s infer to? There is no sensible answer, and it appears as though you're suggesting arbitrary expressions be permitted. If that's not what you're saying, please be clearer.

5 Likes

I see now, thanks, updated the question.

Still doesn't resolve the ambiguity, though, as you could do n.method(s). If you're proposing that resolve to n, I'd be strongly opposed. A bare identifier with as, though? Indifferent at first glance.

2 Likes

I think this is by design to avoid "magic".

Rust often optimizes for clarity when reading, and wants to keep teaching the language simple. n: n as i32 is obvious for users who know struct fields and expressions. Implied binding names from expressions are not so obvious.

If you add support for some expressions, then you have to teach which expressions are fine, and which are not. And have rules for which identifier gets picked up as the field name (is foo(n) allowed? (n as i32) with parens? if n.method() works, does Type::method(&n) too?). It adds several things to the language just to avoid typing n:.

14 Likes

My intuition for the current shorthand works like "if there's no field name - consider it to be the first part of the expression". And I would expect the same to work for the majority of people as it imply only visual pattern matching.

Right. Also the current syntax is familiar for a lot of newcomers from javascript. But still intuitive magic is better than non-intuitive one. Imagine if the current shorthand would work only for variables of &str type and nothing else. It wouldn't make it better in any way. So for me it feels like making the current peace of magic more intuitive rather than adding a new one.

Rust didn't have the shorthand syntax in the beginning. There was a lot of code that had foo: foo pattern, especially in new functions, which was clearly annoyingly redundant. Simplifying it to a single ident was a minimal change that removed the obvious redundancy. The rule that only a single variable name is allowed is relatively simple and unambiguous.

But benefits of taking it further aren't as clear:

  • There are fewer cases to improve. It was common to set every field from variable in the literal, but it's not as common to convert every variable.
  • The cases aren't so clear. There are many kinds expressions of varying complexity, and it's unclear where to draw the line.
  • Foo { bar, baz } removes visual noise compared to Foo { bar: bar, baz: baz }, but OTOH Foo { bar.method(), baz.await?? as Zzz } adds more text, making field names harder to spot.
  • ES6 set a precedent for literal shorthand for single idents, but AFAIK there's no precedent for allowing expressions.
8 Likes

I'd expect them to be mostly vertically formatted, seems easier to spot this way:

Foo {
    bar.method(),
    baz.await?? as Zzz
}

vs

Foo {
    bar: bar.method(),
    baz: baz.await?? as Zzz
}
Foo {
    bar.method(),
    baz.await?? as Zzz
}

Currently you can write this as:

let bar = bar.method();
let baz = baz.await?? as Zzz;
Foo { bar, baz }

Are these intermediate variables really so offensive that you can't stand to write them? If you're trying to match things based on their field names, then they already exist, and you're not avoiding intermediate names. You've just had to define bar and baz further up.

EDIT: For what it's worth, I understand the frustration of having to write long field names twice. The solution I use looks like this:

Foo::new(
    bar.method(),
    baz.await?? as Zzz
)
3 Likes

THIS! As your project grows, you'll spend more time maintaining and fixing things than adding in new features. Being able to jump in and figure out what a chunk of code is doing (clarity) rapidly becomes far more important than any tiny savings you might gain from this. Making n: n become just a plain n is pretty easy to quickly skim and read; anything more complex adds cognitive burden you don't need when reading old code.

7 Likes

Right, but again Foo will usually be formatted vertically, due to the fields lengths and amount. So it would probably take more vertical space, than the proposed option.

If you want to talk about constraints on real code, propose some real code to look at. There are many ways to style code to conserve or segment vertical space if you need to.

2 Likes

Isn't this makes code more error prone, as it relies on fields order rather than their names? Imagine having a few &str fields in the struct, feels easy to screw up the order.

2 Likes

I think that probably the strongest argument you can make for allowing something is to replace today's desugar of Foo { foo: foo } with Foo { foo: foo.into() }. This is already veering into "too magic" because implicit conversions are spoooooky, but ?'s behavior is precedent, at least. (It's certainly not as scary as C++ implicit conversions, because into() can only make one hop on the conversion graph.)

Conveniently, this covers both examples in the OP quite handily.

1 Like

The extension to this that I've seen suggested before is to make it work with &, as something short that doesn't run code.

So, for example, Foo { &foo } would desugar to Foo { foo: &foo }.

That seems potentially plausible, though I'd be curious to see how frequently it can actually help.

Not exactly, because ? is visible. Foo { foo } being able to run arbitrary code without any visible marker is more like Deref.

2 Likes