Pre-RFC: `=` for struct construction

Edit: I’ll update this from time to time

Summary

Introduction of = for struct construction in Rust 2018.

let my_struct = MyStruct { a = 42 }; // New
let my_struct = MyStruct { a: 42 }; // Current

The new syntax is intended to gradually replace the current syntax. Existing code will continue to compile without warnings.

Motivation

Rust uses = in assignment expressions. In contrast to this, when creating an instance of a struct, the : instead of = is used to initialize struct fields with values. This proposal aims to introduce a consistent notation for assignment related operations by introducing = for struct field initialization.

As a consequence, this introduces a clear distinction in notation for assignment related operations and type annotations/definitions/ascriptions. Here are a few examples where : is used to define the type:

// Struct definition
struct User {
    username: String,
    email: String,
}

// Trait bounds
fn print_area<T: HasArea>(shape: T) {
    println!("This shape has an area of {}", shape.area());
}

Instead the use of = introduces similarity to variable bindings:

let my_value = 42; // Assignment
let my_value: u64 = 42 // Assignment with type annotation
let my_struct = MyStruct { a = 42 }; // New struct construction syntax using `=`

This author believes that the improved consistency helps beginners. Furthermore, the notational difference between the definition and construction of structs is valuable to all users of Rust because with it, it becomes possible to tell them apart at a glance.

Guide-level explanation

Shipment strategy

Before Rust 2018 is shipped

  • Introduce support for the = syntax
  • Continue support of : syntax without any warnings
  • Update to rustfix to make easy migration possible
  • Update the documentation for Rust 2018 to use the new syntax
  • Add a rule to clippy, disabled at first

After Rust 2018 has shipped

  • Continue support of : syntax without any warnings
  • Enable clippy rule gradually (different clippy profiles)

Before next edition after Rust 2018

  • New RFC that discusses the deprecation of the old syntax if this is desired

Destructuring and matching

Occurrences of : are replaced with =, the rest of the syntax remains unchanged:

struct MyStruct { x: i32 }
...

let my_struct = MyStruct { x: 42 }; // Current
let my_struct = MyStruct { x = 42 }; // New

// Destructuring: x_var = 42
let MyStruct { x: x_var } = my_struct; // Current
let MyStruct { x = x_var } = my_struct; // New
let x_var = my_struct.x // Without destructuring
// Matching
match Foo { x = 9 } {
    // x
    Foo { x } => {} // Current
    Foo { x } => {} // New
    
    // x_var
    Foo { x: x_var} => {} // Current
    Foo { x = x_var} => {} // New

    // No var
    Foo { x: 1..=10 } => {} // Current
    Foo { x = 1..=10 } => {} // New

    // x_var
    Foo { x: x_var @ 1..=10} => {} // Current
    Foo { x = x_var @ 1..=10 } => {} // New
}

Type ascriptions

This change makes struct construction fully compatible with type ascription in expressions (See RFC803)

let my_struct = MyStruct { a = 42: u64 };

Reference-level explanation

ToDo

Drawbacks

  • Requires many documentation changes
  • Different syntax than other languages: The current syntax mirrors the syntax used in JavaScript. The proposed change would make Rust’s syntax different. It is however similar to the syntax of C99 (explained further below)
  • Struct definition and initialization now look different. Some people might consider this as a drawback. This author thinks that it is actually advantageous.

Rationale and alternatives

  • Keep the current syntax, or
  • Ship the = syntax with a different strategy than what is proposed here

Prior art

Similar syntax in C99

C (and therefore C++ as well) uses almost the same syntax to what is proposed here with the difference that a . is placed in front of each field name.

struct point p = { .y = yvalue, .x = xvalue };

Source

Previous RFC and discussions

Unresolved questions

n/a

7 Likes

Prior art: Similar syntax in C99

You specifically asserted in the other thread that "just because a language uses the syntax doesn't mean that it makes sense". Therefore I find it puzzling that you think "C does this" is a meaningful argument in favor of a wildly breaking change.

It's not exactly assigment though, or not necessarily. You can think of it as assignment, but the notion of a "key-value pair" or "name-value pair" is equally valid, in which case it's not better, it's worse than the status quo. Anyway, such a relatively minor difference in interpretation of a construct doesn't warrant, again, a massively painful change to a very fundamental part of the language. I think this is a very bad idea.

1 Like

This was previously proposed as RFC #65.

It was also recently discussed in the users forum.

5 Likes

I did. And I think that it's more important to have consistency than to do something another programming language does. Nevertheless it is relevant for the discussion to mention that C99 has a similar syntax.

Also, this proposal won't break anything. Instead it proposes continued support of the current syntax.

Can you elaborate what you mean with "It’s not exactly assigment"? It think it assigns values to struct fields.

In procedural speak, you might say “assign”. It’s not changing anything though, so I would actually prefer the term “initializes the field” or “binds the value to the field”. Struct construction is not really about mutating a value, but about constructing a new one from parts.

(I’m not sure what the official terminology for this is, though.)

1 Like

Nit: the : syntax is not used to "define the type" here (I know you didn't mean this, but it could be textually a bit clearer) :wink:

I find the main advantage of this proposal to be the freeing up of the type ascription syntax from the troubles it has been in. I think the proposed syntax is certainly more internally consistent than the status quo. Using the judgemental form x : τ for initializing/updating/binding is deeply strange to me.

That said; Nearly every crate will be touched by this, so this step should only be taken with much care and deliberation.

PS: I think your plan of action ("shipment strategy") is reasonable proviso that we want to take this step.

4 Likes

Someone will need to go over this and correct all the terminology blunders. I'm also pretty sure that the assignment expressions I give as an example are actually just the definitions of a bindings. The compiler guys will probably be aghast and say "these differences should be clear to everyone!" ^^' I just know how to use it

That should already work with the current syntax though, shouldn't it? Isn't let s = MyStruct { a: 42: u64 }; already unambiguous to parse? Field name, colon, then an expression, where the expression can contain an optional type ascription.

And while we are at consistency — another problem with this proposal is this: will it change struct patterns too? If so, will it change them to MyStruct { name = value } as well? Because that doesn't make sense at all:

match foo {
    Foo { name = 1337 } => …,
}

This looks like the field is being assigned the value 1337 in the pattern, whereas it's not, it's a comparison. That's downright confusing.

1 Like

Type ascriptions should work with the current syntax too, but it doesn't look very tidy. I continue to think that type ascriptions aren't useful because the struct has already the types defined. I just wanted to mention how it would look like.

I've just looked at the old RFC and saw that too. Yes IMO it should. It has to be added to the proposal.

Well, that’s even worse (for the reasons I enlisted). That’s not even a subjective consideration, making patterns look like assignments is objectively bad, it’s totally backwards.

4 Likes

Destructuring is IMO precisely assignment backwards. ^^’

1 Like

It’s not though, if you look at the example I posted. Nothing is being assigned here, the struct is not even being constructed. Instead, the name field is being compared against a value. It’s misleading to denote it as an assignment of the value to the field.

If you want to use this particular example, the reader is right to ask: "what about 42u64?". A different example might show your point better.

Unfortunately, no. Given the form:

fn main() {
    struct MyStruct { a: u64 }
    let MyStruct { a: foobar } = MyStruct { a: 1 };
}

I would suspect that this is making the typing judgement a : foobar.

I don't know about "objectively"; but my view is at least that it would be quite confusing / misleading. That said, the current rebind-as syntax is also terribly confusing (with typing judgements).

A possible non-confusing syntax would be:

fn main() {
    struct MyStruct { a: u64 }
    let MyStruct { a as foobar } = MyStruct { a: 1 };
}

To be honest, I don’t see how the as keyword, which is currently only associated with type casting, is better in this sense. It would now gain an overloaded meaning of destructuring, compared to : which is already used for two different purposes in two different contexts. (It wouldn’t be different vice versa either, but that’s history.)

2 Likes

Regardless of whether = or : is the better syntax, this change implies such a tremendously huge amount of churn that I consider it a complete non-starter.

For comparison, when I wrote the dyn trait RFC, I always felt that was just about the maximum amount of churn we could possibly get away with, and hopefully the churniest change we’re ever going to do after 1.0. And that was a case where the existing syntax was so deeply confusing that people often had no idea they were operating on trait objects rather than traits. Even if you don’t like today’s struct syntax, I don’t think anyone’s found :s so confusing that they failed to understand they were initializing a struct.

12 Likes

Darn it! Why is everything so jumbled and why can't we have nice things... :cry:

I wanted it to evoke association with:

use path::to:thing as foobar;

... but I see the possible confusing reading you pointed out.

Well; if we want to go "fully unambiguous" (not saying we should) a syntax could be:

let MyStruct { use a as foobar } = MyStruct { a: 1 };

Another syntax comes to mind..:

let MyStruct { a @ foobar } = MyStruct { a: 1 };
1 Like

Oh, and indeed, as already has another meaning, in imports. In this case that argument of mine doesn’t stand as strong.

(I don’t think we should try making this particular case “fully unambiguous” either, btw – the status quo is pretty clear, but granted, it’s certainly possible to come up with something completely non-overloaded. If this had to be changed, though, I would lean towards @ since it is already familiar as a top-level pattern binder, so it would have a similar function, and it wouldn’t introduce lengthy keywords in a so commonly-used syntactic construct.)

The current destructuring syntax isn’t very intuitive. But yes, if we changed it to use =, like my proposal has intended (didn’t explicitly say so, but it was the intention), it’d make it even worse. Maybe @Centril is right that it should be more verbose with a nice litte keyword that says what’s what? I can say from experience that the destructuring syntax with : can definitely be also very confusing.

For prior art we also have Haskell, which does use = in record pattern matching. See eg. https://en.wikibooks.org/wiki/Haskell/Pattern_matching#Introduction_to_records

2 Likes

Right! I've clearly not written enough Haskell lately; it is showing...

Respectfully I disagree, I find any use of the type-judgemental form to be confusing when it is actually not doing typing judgments.

Perhaps the Struct { field: binding } form is not common enough that it won't be as bad as bricking the field: stuff syntax everywhere, so it's a much smaller delta. It might be worth a crater run to see how much would break and then introduce Struct { field @ binding } which is a much more consistent and natural syntax and then use the other form for type ascription eventually.

1 Like