Pre-RFC: Disallow using assignment in a function call in Rust 2018 (disallow `f(a=b)`)

This has been presented as one of the alternatives, though the conflict with type ascription is quite unfortunate.


I don't see how disallowing Struct { a: b = c } is related to enabling Struct { a = c }. The latter is already invalid syntax today AFAIK.

1 Like

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.

1 Like

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?!

1 Like

@Keats a = 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.

6 Likes

The = for struct construction seems very interesting. I’ll create a new thread for this.

1 Like

Changing the type ascription syntax feels like a bad type of judgement :wink:

Personally I hope we don't; but I have nothing against future proofing :+1:

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).

2 Likes

Note: I’ve added one more rule for disallowing

macro_rules! splat { ($($x:tt)*) => { foo($($x)*) } }
splat!(x = 3); // disallowed

I've created the tread for = in struct construction

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 }.

3 Likes

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).

4 Likes

We already supported named params in println!("{a}", a = value);, so I consider = at least consistent if it were chosen.

2 Likes

It is not really a "named param" per say. This syntax is specific to the println! macro. You could write the same macro using any other token.

4 Likes

I like the idea of this. My opinions:

  • () 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.

1 Like

I consider named arguments to be a bad idea for various reasons. So I don’t think we need this breaking change.

2 Likes

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.

1 Like

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.

2 Likes

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.

12 Likes

Thanks everyone for feedback :slight_smile: I’ve submitted this as RFC 2443.

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