rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

Add closure-move-bindings RFC

Open SOF3 opened this issue 2 years ago • 15 comments

As per discussion in #2407, adds the move(y=f(x)) || {}/move(x) || {} syntax.

Rendered

SOF3 avatar Oct 10 '23 15:10 SOF3

Syntax bikeshed. My brain really wants to parse move(x) as a function call. What if it looked like this?

move { x, y: y.clone() } || foo(x, y);

This syntax is analogous to a struct literal. In that way I think it might be more intuitive, while also less likely to be confused as something it's not (function call) because the lowercase move is an upfront hint that it is not a struct.

camsteffen avatar Oct 14 '23 13:10 camsteffen

[my .02c] @camsteffen I agree the move() may look a bit too much like a call, making the braced syntax appealing; but the issue that could be pointed out, then, is that it doesn't map that well to the shorthand syntax of x.clone() (or my suggested &var):

move { x, &y, w.clone(), z: foo } |…| { … }
  • (it also has the issue of introducing yet another whitespace in this syntax, contrary to () and [] which "stick" to move)

Maybe an in-between, which, for better or for worse, would resemble C++, would be to be using square brackets? 🤷

move[x, &y, w.clone(), z = foo] |…| { … }

Be it as it may, I'd be fine with any choice among these three grouping delimiters, on condition that it not preclude the shorthand capture syntax.

danielhenrymantilla avatar Oct 14 '23 14:10 danielhenrymantilla

async move { x, y, z } { x }

would probably be ambiguous, otherwise I really like that syntax. Though arguably the move should be in front of the async anyway, not sure why it was stabilized this way. Maybe this can be changed on an edition transition to support the move {} syntax.

CryZe avatar Oct 14 '23 14:10 CryZe

If I may add my two bikeshedding cents, I think the syntax is cleanest when name bindings look like let-statements. That is, instead of a move-clause before the closure I propose a move let as the first statement in the closure body.

|| {
    move let foo = foo.clone();
    foo.run()
}

It is of course potentially confusing that the move let statement appears inside the closure body but is executed immediately on closure construction, not invocation. I consider this analogous to static items that may appear inside the body but are executed at compile time:

|| {
    static X : T = f1(); // f1 called once at compile time
    let foo : T = f2();  // f2 called every time the closure is called
    foo.bar(&X)
}

Hopefully the requirement that move let must be the first statement in the closure makes it sufficiently distinct from the rest of the body even without delimiters.

Also, I don't think omitting the let causes any ambiguity, i.e. the syntax could be just move $PATTERN = $EXPR; instead of move let $PATTERN = $EXPR;. You could write move x; as shorthand for move x = x;.

The existing syntax of move before the closure

move || {
    x + y
}

could be interpreted as shorthand for listing every implicitly captured name:

|| {
    move (x,y);
    x + y
}

nhuurre avatar Oct 15 '23 12:10 nhuurre

@nhuurre I think the case for static is a bit different, in that static initializers are always const expressions so it does not matter when they are executed, but move initializers may have side effects and the execution time matters. After all, static expressions are executed during compile time instead of during "static constructors" (or whatever relevant concept) as you put it akin to.

SOF3 avatar Oct 15 '23 12:10 SOF3

@camsteffen maybe it's just me, but move { foo } || foo() looks like an expression joining a struct literal and a function call with a logical or as well.

That said, it has never occurred to me (maybe it does to those unfamiliar with Rust) that the following looks like a logical or expression:

move || foo()

As with @danielhenrymantilla's comment, it is equally likely to read move[foo] || foo() as a logical or expression with an array index.

Maybe it's because almost every syntax highlighter (no matter semantic or lexical) always highlights the move keyword? In that case, it would be obvious that move() is not a function call because it involves a keyword, as much as break (a, b) wouldn't cause ambiguity that we are calling a function called break with arguments a and b. From this perspective, perhaps it's better to leave a space between move and the ( in the style guide?

SOF3 avatar Oct 15 '23 12:10 SOF3

I have previously written a draft RFC around adding capture clauses -- I still like that design. I'd love to see it compared/contrasted with this one (haven't had time to fully read the RFC yet), as well as to know whether this one achieves the various bits of motivation.

One obvious difference is that I did not support move(x = y+1), though that would be an easy extension. I did support move(x.clone()) which would effectively be shorthand for move(x = x.clone()).

nikomatsakis avatar Oct 19 '23 17:10 nikomatsakis

Based on the comments above, I am planning to separate the RFC into two features:

  • closure_move_bindings:
    • Introduce the syntax move(foo = bar) || expr and move(mut foo = bar) || expr, where foo is an ident and bar and expr are expressions. This is just an alternative syntax for { let mut foo = bar; || expr} with the appropriate move semantics.
    • Introduce the syntax move(foo) || expr and move(mut foo) || expr, where foo is an ident. This is an alternative syntax for move(foo = foo) || expr.
  • closure_move_binding_method_call: Introduce the move(foo.bar()) || expr syntax, where foo and bar are idents. This is an alternative syntax for move(foo = foo.bar()) || expr.

SOF3 avatar Nov 19 '23 12:11 SOF3

Another issue: Is it better to use : instead of = to separate the pattern and the expression? That would be more consistent with the field: expr syntax in struct literals, which would make more sense if field: field can be shortened into field. Or is it better to reserve : for type annotations in the future?

SOF3 avatar Nov 23 '23 03:11 SOF3

@SOF3, regarding name: value vs. name = value:

  • it does have a nice "symmetry" with braced struct syntax, assuming braces being used as delimiters;
  • this, in turn, "curses" the syntax not to have too much feature creep, lest there be inconsistencies between braced struct syntax and this closure capture one:
    • move(&x, &mut y) would have to become move { ref x, ref mut y } (and there seems to be kind of an implicit but general consensus that ref [mut] is confusing for newcomers and something which new syntax should try to avoid).
    • what about move(var.method())? No such braced syntax.

So even if the different shorthands end up split as follow-up features / RFCs, if keeping them in mind, we may actually want to avoid var: value syntax for the captures of this RFC

danielhenrymantilla avatar Nov 27 '23 14:11 danielhenrymantilla