MyStruct { a: 7 } could just be deprecated very gently (no breaking change) and rustfix could update existing code very quickly to MyStruct { a = 7 }. It'd make usage of type ascription (e.g. MyStruct { a = 7: u64 } in struct construction possible. Not bad if you ask me. (Edit: Although it's probably worthless because the struct is already typed :D)
That said, this RFC has minimal impact and it keeps the door open for both ways. Whatever the choice I'd prefer consistency between these two notations.
I wasn’t even aware this was possible and can’t really see any use for it so I would support disallowing it, as well as in struct if it’s currently allowed. I doubt that anyone is using that “feature” on purpose?!
@Keatsa = b is an expression like any other. That’s why you can use it in situations like
match expr {
pat => a = b,
}
It’s just that, since it has type (), it’s not a very useful expression. Banning it in certain situations would complicate Rust’s syntax though, it could break macros for instance.
This makes me wonder whether a = b should be an expression at all. Maybe match arms like that should require a block to be written:
match expr {
pat => { a = b; },
}
Alternatively, make match the special case and have an extra syntax rule for => a = b (are there any other places where assignment-as-an-expression is useful?).
Edit: Also +1 to changing the struct expression syntax to MyStruct { x = y } in the new edition.
Changing the type ascription syntax feels like a bad type of judgement
Personally I hope we don't; but I have nothing against future proofing
If y'all want to change the struct initialization syntax to field = expr or at least not allow field: value and field: binding, then you need to at minimum crater run this to see the extent of the breakage. My hunch is that the breakage extent makes it wildly undoable. I think nearly every crate would be broken by this change or start emitting warnings (depending on strategy).
I think I like this one best, since it feels like it'd have fewer edge cases (I have no idea what tokens vs not tokens are supposed to mean for macros here, for example), and one can easily turn the a = b statement into an expression as { a = b }.
I guess this can’t hurt, but I think named arguments will end up requiring a separator (sigil or keyword) such as foo(x ; a = a) so that we can use the sugar foo(x ; a) without being confusing. Though foo(; a) is a bit ugly. We also need a separator to support patterns such as foo(; Pat(a) = a).
Usage of “=” for struct construction will likely not happen. With that in mind, reserving = for usage in named parameters in function calls doesn’t make a lot of sense. For consistence within Rust, the syntax foo(name: arg) would be the more logical syntax choice for named params (like Swift).
() is a special type that in many cases its use can be optimized out or implied. So it is fairly reasonable to make special rules to only this type, and I believe the rule should apply to all () returning expressions, not just assignment expressions.
Rather than restricting some special uses of this type, we may want to think about the use cases of this type and define special rules that apply to this type only, so it wouldn’t affect the beauty of the type system.
For example, I suggest that any expression that returns () should not be used as function parameter or assignments (including struct constructions), except within an explicit block like (a=b) or {a=b}. This also apply to f(drop(a)) where you have to write f((drop(a))).
The error message (or lint message) may read “An expression of type () must be placed in braces”.
Examples
fn identical<T>(t:T) -> T {
t
}
fn do_nothing() {
}
fn main(){
let (mut a,b)=(1,1);
//Illegal: `a=b` returns ()
//identical(a=b)
identical((a=b));
//Illegal: match statement returns ()
//identical(match () { () => do_nothing() })
identical(match () { () => 1 }); //Ok, match statement does not return ()
//Illegal
//After type inference, identical(()) returns ()
//let v = (identical(()),2)
}
Changes on teaching
Before
() is the type, () is the value, { a=b; } is shorthand for { a=b; () }
After
() is the type, <empty expression> is the value. To specify that we have a value that can be represented by an empty expression, we need to put it inside () otherwise it will lead to expressions like f(,1) or let a = ;, which is wired and we don’t want to allow. { a=b; } is legal because it contains a statement and an empty expression. (and empty expression is allowed as the returning piece of a block). Another piece that empty expression is allowed is in a return statement.
This distinction could be found in OCaml record syntax:
type point2d = { x : float; y : float } is record type declaration and let p = { x = 3.; y = -4. } is constructing record.
I tend to agree that = is a little bit noisy comparing to subtle :, but for me benefits outweigh. It’s more consistent and also saves us from the colon overloading plague (it’s anectodal that among all special characters : gets the most proposals to be overloaded in many languages; Perls folks even banned such proposals AFAIK). Good to have : only for types to save brain from too much context analysis.
In my proposal for named parameters, the syntax f(a => b) is used. It solves the ambiguity with structs and assignments without breaking anything since it’s truly a new syntax.
Ouch. First, having to write workarounds in order to express () is very annoying. Second, this is inconsistent with tuple notation. (Unit is literally a zero-element tuple, it's not "special" as you asserted.)
Ouch. It might not be obvious, but that is a very bad idea. Restricting () from being used in many contexts either butchers its usefulness or introduces a lot of noise, and it disregards an important aspect of generics. If () can't be used like any other value and has to be surrounded by braces, parentheses, or any other special marker, then one of two (bad) things will happen:
Generic functions and types which are not written with this arbitrary restriction in mind will not be usable with (). That's a huge disadvantage, because lots of such practical code exists; e.g. HashSet is implemented in terms of a HashMap<Key, ()>.
Or, every generic function and method will have to be written with "I can't use () in certain positions" in mind, and thus they will have to have extra parentheses and braces sprinkled all over them. That is basically pure noise, and doesn't help legibility or clarity at all.
Your "check for () after type inference" idea also has one more serious implication. It means that generics will behave differently post-monomorphization, i.e. the compiler has to re-typecheck everything generic after monomorphization. This is also the wrong thing to do, since it basically breaks the fundamental contract of parametric polymorphism. We would be back to C++'s templates on square 1 with this suggestion: we could no longer be sure that a generic function will compile and be usable just because its definition compiled.