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


#21

Note: I’ve added one more rule for disallowing

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

#22

I’ve created the tread for = in struct construction


#23

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


#24

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


#25

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


#26

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


#27

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.


#28

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.


#29

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


#30

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.


#31

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.


#32

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.


#33

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