Pre-RFC: `=` for struct construction

You have clearly written too much Haskell lately; it is showing… :stuck_out_tongue:

Like as, I consider the : syntax to have different meanings depending on the context. Just like for is not called "the loop syntax", because it means a loop in statement position but a type specification inside a trait impl, I wouldn't go so far as calling any colon-based syntax "the type judgmental form".

Do you mean in patterns specifically?

The @ destructuring syntax is growing on me

1 Like

I guess if we’re all talking about destructuring syntax instead of construction syntax (I have no idea what the actual proposal is now, but oh well), we should bring up Javascript’s destructuring syntax as prior art: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment This uses :s, and afaik people generally like it, and it also does { field: binding }. I always felt the behavior was a little odd when you have both a field and a binding, but all the alternatives feel equally odd to me.

2 Likes

@Ixrec The destructuring syntax exists already (Playground Link) We’re discussing it because = instead of : makes it less clear.

I was under the impression people were suggesting changing let Foo { a: b } = Foo { a: 42 }; to let Foo { a = b } = Foo { a: 42 }; or something like that. Though this thread has gotten very confusing.

@Ixrec Exactly. And let Foo { a = b } = Foo { a: 42 }; would be a mess.

let Foo { a @ b } = Foo { a: 42 };on the other hand is much better.


(And let Foo { a: b } = Foo { a: 42 } is the one that is problematic in conjunction with type ascriptions)

I was thinking of implementing a new monad transformer just this day!

But I am reading a paper on Dependent Information Flow Types right now :wink: -- interesting stuff.

Fair enough I guess I was being too judgemental; ^,-

But I would like a way to do type ascription in pattern matching.

Yeah, at least for initialization there is a different way to type-ascribe.

I’ve added destructuring to the proposal above.

For more prior art: Erlang uses = in records, but the confusing part about = in Erlang records is that the LHS in that case is not a variable or a value, it’s a compile-time field name that doesn’t even make it into the first intermediate language (Core Erlang). Whereas, = in Erlang function bodies is called the “match” operator, and within patterns means to bind a variable to a destructured value (similar to Rust’s current @) [edit: It actually looks like = within patterns still means “match”, but I only ever use it in the way I mentioned]

I really like C99 syntax. It’s also greppable, because .foo = is in both literals and object assignments.

Freeing : to always be before a type, not value, will make grammar simpler and allow new constructs.

Churn is problematic though. We first need tools and work out whether we can manage such a huge transition. Manually launching rustfix and answering “yes” hundreds of times is not cutting it.

5 Likes

Proposed syntax is IMHO more natural and makes more sense (I recently tried to generate a mini Rust parser and, too, was wondering why it was finally decided that struct initialization uses "field: init_expr" syntax...).

However, churn argument aside, if we're trying to make things more consistent, I believe we should watch out for changed destructuring syntax:

This means that b is a binding to (value at) a. However, this is inverted for what we currently have in Rust (playground), where ident @ pat means we bind pat as ident, not the other way around (For prior art, this is the same in Haskell). In this proposal, I believe we should stick with that and prefer using let Foo { b @ a } = ...;, where a is the Foo::a field and b is the newly created binding to it.

Nice catch! Of course it should be let Foo { bind @ pat } = Foo { a: 42 }

1 Like

Oh damn. Well that doesn’t fit.

enum Message {
    Hello { id: i32 },
}

// let msg = Message::Hello { id: 5 }; // current
let msg = Message::Hello { id = 5 }; 

match msg {
    // Message::Hello { id: id_variable @ 3...7 } => {} // current

    // What now?
    Message::Hello { id = id_variable @ 3...7 } => {} // Alternative 1
    //Message::Hello { id_variable @ id = 3...7 } => {} // Alternative 2
}

I’d prefer alternative 2. But it changes the placement of the @ delimiter :thinking:and there might be a reason for why it’s placed there and not in front.

I’ll update the proposal again. Edit: Done. I only show alternative 2 in it. Tell me what you think. I’m not too optimistic that a solution that uses @ is workable. Reordering the placement of @ in a match pattern is probably impossible and the placement I show in alternative 1 doesn’t work for destructuring if I see this correctly

Edit: Alternative 1 is better. The other one is probably impossible to implement.

Edit: Here’s the code I added above. Let’s discuss if this can work. The existing rules for @ in patters are a bit baffling

let bar = Bar::Baz { a = 5 }; 

match msg {
    Bar::Baz { a: a_var @ 3...7 } => {} // Current
    Bar::Baz { a = a_var @ 3...7 } => {} // New

    Bar::Baz { a: a_var } => {} // Current
    Bar::Baz { a_var @ a } => {} // New
}

I've noticed that, "How Haskell does it," comes up A LOT when discussing these kinds of things. I'm not sure whether or not that is always a good thing or ever a bad thing, but, it does make me wonder if there is a blind-spot with the language team and those closely interested in changes to the Rust language that encourages, more than possibly appropriate, to rely on how Haskell does things as the ultimate justification or road-block for or against a particular proposal.

I myself, am not intimately familiar with Haskell, but, I've done extensive reading on it and I do like it as a language, but, about a year ago when I decided it was time to "Learn another new Language", I spent some significant time reading up and researching for myself all the "Newish" languages that I had not been paying attention to for several years. I examined in depth: Haskell, Elm, Swift, Go, Kotlin, Scala, D, and Rust (and others that I can't recall at the moment). Of them all, I found myself interested in Rust and I felt that what it had to offer seemed like the best way forward. I was particularly attracted to the borrow checker, lifetimes, fearless concurrency, enums, matching, etc. In other words, I chose to dive into Rust for what Rust was not for how it was similar to anything else.

So, why am I bringing this up? Well, I just want to shine a light on the possibility that how one particular language does things might unintentionally, become the primary justification for how things are done in Rust. I personally do not think that would be a good thing. That is not to say that how things are done in Haskell should be ignored or shouldn't be brought up, but, it would be nice to see some more comparisons to other languages more often as well. Also, it would be nice to see more argument for or against a proposal not based on similarity to another language, but, more on internal consistency within the Rust language itself.

These are just some thoughts I've been having on these sorts of proposals. Not to impune any particular language or anyone personally, just drawing attention to the possibility of some "Blind Spots" in these sorts of decisions.

4 Likes

@gbutler Haskell mainly comes up a lot because a) @Centril is a big fan of it and b) Rust is in many regards similar to it and constantly steals features (mainly type system features) from it (at least I think it does) I personally have no experience with Haskell whatsoever.


I did some thinking and I think @ cannot be used for destructuring because it clashes with how it’s used in match patterns. Can’t put my finger down exactly where it breaks but I’m relatively certain that it won’t work. Maybe someone else can analyze this more.

The thing about Haskell is, though, that Rust borrows a lot of its semantics, but not its syntax. And that’s not an accident. There are many good parts in Haskell’s semantics, but its syntax is often convoluted, irregular, or just non-conventional, because it’s a research language, a melting pot of many ideas. Rust doesn’t aim to be a research language, so apart from the nice theoretical gains, very concrete and practical things such as a regular, easy-to-understand, robust syntax, also weigh a lot in its design.

5 Likes

The use of @ for destructuring looks ugly and out-of-place to me. I'm not sure why. It just seems like a "Turd in a Punch-Bowl" for some reason. Probably because @ is infrequently used in computer languages and when it is used, it is often used without spaces around it. It just feels out-of-place for this. I know that is wholly subjective and not any sort of useful analysis, but, sometimes things just rub the wrong way. I think also the fact that @ means "at" it just feels weird to read, "a_var at a" means "pull the value of a from the structure and bind/assign it to a_var". It just doesn't seem to "fit". If there were an operator that meant (or at least elicited) "from" it would read better. Can a keyword, like "from" be reasonably used instead of "@"? Or perhaps a 2-character operator instead. Some proposals (in order of what I would personally prefer):

  • let Foo{ a_var << a } = foo;
  • let Foo{ a_var <: a } = foo;
  • let Foo{ a_var <- a } = foo;
  • let Foo{ a_var :< a } = foo;
  • let Foo{ a_var from a } = foo;
  • let Foo{ a_var of a } = foo;
2 Likes

I like the let Foo{ a_var from a } = foo syntax. Here is some code:

I think it’s nicer and more intuitive than what we currently have. But, the churn o_O

struct MyStruct { x: i32 }

enum MyEnum {
    A { a: i32 },
    B { b: MyStruct },
}

let my_struct = MyStruct { x: 42 };

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


// Matching
let my_enum = MyEnum::A { a: 9 }; // Current
let my_enum = MyEnum::A { a = 9 }; // New

match my_enum {
    // a
    MyEnum::A { a } => {} // Current
    MyEnum::A { a } => {} // New
    
    // a_var
    MyEnum::A { a: a_var} => {} // Current
    MyEnum::A { a_var from a } => {} // New

    // No var
    MyEnum::A { a: 1..=10 } => {} // Current
    MyEnum::A { a = 1..=10 } => {} // New

    // a_var
    MyEnum::A { a: a_var @ 1..=10} => {} // Current
    MyEnum::A { a_var from a = 1..=10 } => {} // New

    // No var
    MyEnum::B { b: MyStruct { x: 42 }} => {} // Current
    MyEnum::B { b = MyStruct { x = 42 }} => {} // New

    // b_var
    MyEnum::B { b: b_var @ MyStruct { x: 42 }} => {} // Current
    MyEnum::B { b_var from b = MyStruct { x = 42 }} => {} // New

    // x
    MyEnum::B { b: MyStruct { x }} => {} // Current
    MyEnum::B { b = MyStruct { x }} => {} // New

    // x_var
    MyEnum::B { b: MyStruct { x: x_var }} => {} // Current
    MyEnum::B { b = MyStruct { x_var from x }} => {} // New

    // x_var
    MyEnum::B { b: MyStruct { x: x_var @ 1..=10 }} => {} // Current
    MyEnum::B { b = MyStruct { x_var from x = 1..=10 }} => {} // New
    
    _ => {}
}

Edit: I just want to mention that the syntax I’m labeling as “current” is available in Rust > 1.26 (..= range syntax in particular) Try in Playground

Edit: The proposal above uses now this syntax.

2 Likes

The logic of pattern matching is that you take the literal syntax for constructing a value and use it to destructure a value, putting bindings in place of the values they are to be assigned. If constructing a variant has the syntax MyEnum::A { a = <value> }, then destructuring it has the syntax MyEnum::A{ a = <pattern> }. @ syntax fits into the picture by allowing this:

pattern = /* ... */
pattern = binding
pattern = binding '@' pattern

which lets you both bind and do further pattern matching on the same value.

Having MyEnum::A{ binding @ fieldName } doesn’t make sense, because binding @ fieldName isn’t taking the place of a value in the MyEnum::A constructor. And from syntax breaks the logic of pattern matching, because the binding is no longer taking the place of the value it is being assigned in the pattern syntax.

7 Likes

@steven099 This asymmetry is intentional. Without from syntax:

let MyEnum::A { a = b } = my_struct; // What's the binding? a? b? (Answer: b)

Instead with from this relationship is clear at a glance:

let MyEnum::A { b from a } = my_struct; // b is the binding

The = for struct construction proposal doesn’t necessarily need a change to the pattern matching syntax. And the pattern matching syntax change should get its own RFC if we decide it is worth pursuing. The discussion in this thread however brought us there and it’s very interesting how a syntax that fully embraces = can look like.

I am curious if anyone can find any problems with the proposed syntax. Also criticism is very welcome.