Pre-RFC: `=` for struct construction

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.

Does from also work here?

if let MyEnum::A{ Some(b) from a } = value { ... }

A binding is a pattern. Whatever syntax matches a field to a pattern will also match that field to a binding, so having a separate syntax for specifically matching a field to a binding wouldn't really work.

It doesn't really seem like we're "fully embracing =" if we don't read MyEnum::A { a = b } as "field a has value b" in all contexts :wink:.

Anyways, as far as the consistency argument goes, with

struct S(i32, f64);
...
let s = S(5, 6.2);

the values assigned to each field of the struct take the places of the types in the constructor declaration. With labelled fields

struct S { a: i32, b: f64 }
...
let s = S{ a: 5, b: 6.2 };

the values are still taking the places of the types. In this context _: i32 isn't serving as a type ascription in the first place, instead a: _ is serving the role of a field label.

1 Like

That's because Haskell is an important language when it comes to the field of Type Theory, Programming Language Technology (PLT), which we have to deal with when doing language design.

That said; Rust is not at all syntactically close to Haskell, but in this particular respect of @, Haskell's syntax is actually aligned with Rust's, so it is not strange that it is brought up as prior art.

I'm not saying that you're doing it, but what I find interesting / strange is that sometimes, Haskell is uniquely criticized when used as prior art; Somehow, some people feel comfortable brushing of Haskell as a non-mainstream language.

Well; Haskell is certainly not new -- it is older than Java :wink:

Fearless concurrency is marketing ^,- It is equally true of Haskell (probably truer thanks to pure semantics...). Enums and matching also exist in most ML-derivatives (including Haskell and Rust).

The unique thing about Rust is combining C's abstract machine and a region/lifetime based approach in addition to great abstraction mechanisms to get close to the expressive power of a language such as Haskell.

EDIT: Another killer feature of Rust is its community. This is often overlooked, but we have a very friendly , respectful and helpful community and that is super important!

I would not say that Haskell is particularly brought up more so than many other languages so I am not sure this is reflective of reality. I can only speak for myself, but I regularly cite other, often imperative, languages as well when writing about prior art, etc.

As I mentioned above, Haskell was brought up as prior art here because it was relevant and also coincided with Rust itself.

While I might be unabashedly up-front with my love of Haskell, I am by no means alone :wink:

Wrt. semantics and the type system (notably traits, which are fundamental to Rust...) you are quite right.

I don't mean to derail the topic, but since it was brought up... (if you want, we could start a new topic about it or discuss it somewhere else..)

These points about syntax are all subjectively held beliefs of yours with no basis in objective fact as far as I can tell (and until you show me scientific research that the layout syntax of a Haskell like language is bad, I won't be convinced of it either...).

When Haskell was designed, a syntax tzar (one person) was in charge of making sure that the syntax of the language was internally consistent -- and they did an excellent job in my opinion; I believe this is still very much true today. Sure, there are some syntax extensions from language pragmas that very few use, but most of them are also quite regular wrt. how the language works today. The extensions are also not part of the Haskell report.

Haskell is not just a research language. Interesting research cutting edge research is being done in it, but it is by no means an impractical language, far from it. Haskell was designed to be practical, and Haskell is practical. If you want type safe but rapid prototyping (on par with Python or faster..) and productivity, Haskell is an excellent choice.

While Rust is not a research language, interesting research is being done in conjunction with Rust. See for example the RustBelt papers.

Rust's syntax is as it is to be familiar for C, C++, Java programmers; not because one syntax is objectively better than the other, but because if you want people to be OK with adjusting to a borrow checker and the type system at large, the unfamiliarity of a different lexical syntax is not a place to innovate, so as to not place an extra burden when learning.


This point is well made. I retract my support for bind @ pat.

4 Likes

Good point. Yes, that is how it should work (if we decide we want the from destructuring method)

I'm not opposed at all to actually just replacing the : with a =. It'd mean that we get the benefits of the benefit of this proposal which is consistent usage of = and : without the ginormous churn that changing the stable destructuring syntax entails.

The tuple struct syntax has no :, so it already works nicely.

Exactly. This proposal aims to make the langugage consistent by removing such cases. : should always refer to something type related

I've explained at least once to you with reasonable arguments why some aspects of Haskell's syntax are bad. You might not be convinced by those arguments, and that's fine.

1 Like

You, have, and that’s fine (to which I gave my subjective counterpoints); but they are still subjective.

If you present me with some peer reviewed studies to the contrary, I will review those and agree with you if the facts are borne out by evidence.

In that case, it would also be:

struct S { x: i32, y: i32 }
...
match S { x = 1, y = 5 } {
    S { 6 from x, 2 from y } => ...,
    S { _ from x, 4 from y } => ...,
    S { 1 from x, 1 ... 6 from y } => ...,
    S { _ from x, _ from y } => ...,
}

I really don't like this. It's all well and good to say that it's intentionally asymmetrical, except that the whole point of patterns is that they're symmetrical with literal expressions, with bindings in place of variables and some niceties (range patterns, binding @ pattern, ref [mut]).

Moving away from this is a big deal as far as allowing people to reason about what the pattern corresponding to a given expression is. If you think you can have all those patterns with S { x = ..., y = ... }, then that's great. If not, then afaic, = isn't the right operator, and messing with how patterns work to fix it is just making things worse. Maybe it could be => or something, or just leave it as : and accept that : doesn't always denote a type annotation (leaving aside that it also denotes a constraint like in T: Trait).

3 Likes

I do like using = for both literals and patterns, since it would allow shorthand to be compatible with

I think the proposed syntax would have been a better choice before 1.0. It’s easier to parse, and avoids ambiguities between, say, keyword parameters and type ascription (both of which would be really nice to have in Rust). But at this point, I’m not sure it’s worth the cost of changing it.

11 Likes

@steven099 The code that you’re showing is not how the proposed syntax would work. from would only be used to introduce bindings, not to compare.


So, to summarize. I currently don’t think that this will go forward. Here’s why:

  • The churn is very high (documentation changes, eventually code changes). Unlike dyn Trait what’s gained, helps a bit, but in comparison not nearly as much with explanations in the documentation.
  • The current syntax is compatible with type ascriptions, as pointed out here by @H2CO3. Doesn’t look pretty, but it looks good enough. Besides, since structs are typed, I doubt that type ascriptions will be used a lot.
  • The : is inconsitent to other assignment-like operations which use =. But, the syntax is consistent between struct construction and patterns for destructuring, match and if let because they all use :.
  • The usage of = in patterns feels less readable. Objectively not much changes when : is replaced with =. Subjectively, however, the data flow is somehow less clear. The proposed from syntax tries to solve that, but changing it would just mean a lot of churn for negligible gain.
  • The similarity to JavaScript is lost. Rust wants to be the goto language for Web Asssembly. Familiar syntax helps JavaScript programmers adopt the language.
2 Likes

While I agree, it would still have use for generic structs, say a pattern like Range { start: u8, end }. (cc Clippy lint proposal since that's valid, but very confusing, syntax today.)

1 Like

@scottmcm I agree that this is a danger. I’ve added a comment in the issue.

When you're matching against Some(b) as we discussed above, you're doing both a comparison and a binding. This is maybe even more clear if you match against (b, 5). Pattern matching inherently interleaves comparison and binding, and it works because they aren't different, but instead are both instances of matching a value to a pattern. All along I've been emphasizing how pattern matching works, that the destructuring pattern for a composite value is the same as the constructor for the value with its element values themselves replaced with patterns, and that bindings, range patterns, and even numeric literals are all patterns.

The syntax I showed was intentionally different from what you were proposing. I wasn't creating a strawman to knock down; the intent was to impose consistency on the design in order to highlight where it is inconsistent, namely in trying to make a distinction between binding and comparison that doesn't make sense in view of how pattern matching works. If you write a = 5 and b from a, then you write a = (1, 3) and (b, c) from a, and... (b, _) from a = (_, 5)? I'm really not sure how it is intended to work at the boundary, or where the boundary is—where it even can be—and that's essentially my point.

1 Like

@steven099 I see what you mean. The from syntax cannot be applied consistently. It wants to avoid comparisons to fields via “=”, but fails to do so in some cases.

from syntax

match Foo { a = (1, 2) } {
  Foo { a_var from a } => {} // a_var

  Foo { a = (x, 2) } => {} // x, <-- Problem: Binding on left side of `=`
  Foo { (x, _) from a = (_, 2) } => {} // x, <-- Or a real mess
}

The current syntax on the other hand is consistent:

match Foo { a: Some(42) } {
  Foo { a: a_var } => {} // a_var

  Foo { a: (x, 2) } => {} // x
}

Edit: I’ve updated the proposal. It now just uses “=” instead of “:” and leave the rest unchanged

Okay. My comments were following from our discussion of matching a field against Some(b), but I saw from your proposal that it would have allowed Foo{ a = (b, 5) } and only used from for Foo { b from a = (_, 5) }, and thus not Foo { (b, _) from a = (_, 5) }. As you observed, if the goal is to not have bindings on the right-hand side of = since this would be the opposite of let ... = ..., then this is a problem.

You could—and I believe have—argued that the main problem being addressed is not knowing which is the field name in Foo { a = b }, and that once you add structure to make it Foo { a = (b, 5) }, there is no longer any potential for confusion. I personally don’t think creating such a narrow exception to pattern syntax is a good idea, and if Foo { a = <value> } were accepted to mean "field a has value <value>", then it ought to be accepted to mean that in all contexts.

It also seems unlikely at this point that this proposal will be accepted either way though, so I’m not sure it matters much.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.