-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Add closure-move-bindings RFC #3512
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 3 commits
3c2d8b6
d8a1461
968b93e
ea48e5d
7226c61
c3d152a
5c38456
70a14b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,284 @@ | ||
- Feature `closure_move_bindings` | ||
- Start Date: 2023-10-09 | ||
- RFC PR: [rust-lang/rfcs#3512](https://github.com/rust-lang/rfcs/pull/3512) | ||
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) | ||
|
||
# Summary | ||
[summary]: #summary | ||
|
||
Adds the syntax `move(bindings) |...| ...` | ||
to explicitly specify how to capture bindings into a closure. | ||
|
||
# Motivation | ||
[motivation]: #motivation | ||
|
||
Currently there are two ways to capture local bindings into a closure, | ||
namely by reference (`|| foo`) and by moving (`move || foo`). | ||
This mechanism has several ergonomic problems: | ||
|
||
- It is not possible to move some bindings and reference the others. | ||
To do so, one must define another binding that borrows the value | ||
and move it into the closure: | ||
|
||
```rs | ||
{ | ||
let foo = &foo; | ||
move || run(foo, bar) | ||
} | ||
``` | ||
|
||
- It is a very frequent scenario to clone a value into a closure | ||
(especially common with `Rc`/`Arc`-based values), | ||
but even the simplest scenario requires three lines of boilerplate: | ||
|
||
```rs | ||
{ | ||
let foo = foo.clone(); | ||
move || foo.run() | ||
} | ||
``` | ||
|
||
This RFC proposes a more concise syntax to express these moving semantics. | ||
|
||
# Guide-level explanation | ||
[guide-level-explanation]: #guide-level-explanation | ||
|
||
A closure may capture bindings in its defining scope. | ||
Bindings are captured by reference by default: | ||
|
||
|
||
```rs | ||
let mut foo = 1; | ||
let mut closure = || { foo = 2; }; | ||
closure(); | ||
dbg!(foo); // foo is now 2 | ||
``` | ||
|
||
You can add a `move` keyword in front of the closure | ||
to indicate that all captured bindings are moved into the closure | ||
instead of referenced: | ||
|
||
```rs | ||
let mut foo = 1; | ||
let mut closure = move || { foo = 2; }; | ||
closure(); | ||
dbg!(foo); // foo is still 1, but the copy of `foo` in `closure` is 2 | ||
``` | ||
|
||
Note that `foo` is _copied_ during move in this example | ||
as `i32` implements `Copy`. | ||
|
||
If a closure captures multiple bindings, | ||
all the `move` keywoed makes them all captured by moving. | ||
SOF3 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
To move only specific bindings, | ||
list them in parentheses after `move`: | ||
|
||
```rs | ||
let foo = 1; | ||
let mut bar = 2; | ||
let mut closure = move(mut foo) || { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just noticed that GitHub renders There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
foo += 10; | ||
bar += 10; | ||
}; | ||
closure(); | ||
dbg!(foo, bar); // foo = 1, bar = 12 | ||
``` | ||
|
||
Note that the outer `foo` no longer requires `mut`; | ||
it is relocated to the closure since it defines a new binding. | ||
|
||
Moved bindings may also be renamed: | ||
|
||
```rs | ||
let mut foo = 1; | ||
let mut closure = move(mut bar = foo) || { | ||
foo = 2; | ||
bar = 3; | ||
}; | ||
closure(); | ||
dbg!(foo); // the outer `foo` is 2 as it was captured by reference | ||
``` | ||
|
||
Bindings may be transformed when moved: | ||
|
||
```rs | ||
let foo = vec![1]; | ||
let mut closure = move(mut foo = foo.clone()) || { | ||
foo.push(2); | ||
}; | ||
closure(); | ||
dbg!(foo); // the outer `foo` is still [1] because only the cloned copy was mutated | ||
``` | ||
|
||
The above may be simplified to `move(mut foo.clone())` as well. | ||
|
||
This simplification is only allowed | ||
when the transformation expression is a method call on the captured binding. | ||
|
||
# Reference-level explanation | ||
[reference-level-explanation]: #reference-level-explanation | ||
|
||
A closure expression has the following syntax: | ||
|
||
> **<sup>Syntax</sup>**\ | ||
> _ClosureExpression_ :\ | ||
> ( `move` _MoveBindings_<sup>?</sup> )<sup>?</sup>\ | ||
> ( `||` | `|` _ClosureParameters_<sup>?</sup> `|` )\ | ||
> (_Expression_ | `->` _TypeNoBounds_ _BlockExpression_)>\ | ||
> _MoveBindings_ :\ | ||
> `(` ( _MoveBinding_ (`,` _MoveBinding_)<sup>\*</sup> `,`<sup>?</sup> )<sup>?</sup> `)`\ | ||
> _MoveBinding_ :\ | ||
> _NamedMoveBinding_ | _UnnamedMoveBinding_\ | ||
> _NamedMoveBinding_ :\ | ||
> _PatternNoTopAlt_ `=` _Expression_\ | ||
> _UnnamedMoveBinding_ :\ | ||
> `mut`<sup>?</sup> ( _IdentifierExpression_ | _MethodCallExpression_ )\ | ||
|
||
> _ClosureParameters_ :\ | ||
> _ClosureParam_ (`,` _ClosureParam_)<sup>\*</sup> `,`<sup>?</sup>\ | ||
> _ClosureParam_ :\ | ||
> _OuterAttribute_<sup>\*</sup> _PatternNoTopAlt_ ( `:` _Type_ )<sup>?</sup> | ||
|
||
Closure expressions are clsasified into two main types, | ||
SOF3 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
namely _ImplicitReference_ and _ImplicitMove_. | ||
A closure expression is _ImplicitMove_ IF AND ONLY IF | ||
it starts with a `move` token immediately followed by a `|` token, | ||
without any parentheses in between. | ||
|
||
## _ImplicitReference_ closures | ||
|
||
When the parentheses for _MoveBindings_ is present, or when the `move` keyword is absent, | ||
the closure expression is of the _ImplicitReference_ type, where | ||
all local variables in the closure construction scope not shadowed by any _MoveBinding_ | ||
are implicitly captured into the closure by shared or mutable reference on demand, | ||
preferring shared reference if possible. | ||
|
||
|
||
Each _MoveBinding_ declares binding(s) in its left-side pattern, | ||
assigned with the value of the right-side expression evaluated during closure construction, | ||
thus referencing any relevant local variables if necessary. | ||
|
||
If the left-side pattern is omitted (_UnnamedMoveBinding_), | ||
the expression must be either a single-segment (identifier) `PathExpression` | ||
or a _MethodCallExpression_, | ||
the receiver expression of which must be a single identifier variable, | ||
and the argument list must not reference any local variables. | ||
The left-side pattern is then automatically inferred to be a simple _IdentifierPattern_ | ||
using the identifier/receiver as the new binding. | ||
|
||
### Mutable bindings | ||
|
||
If a captured binding mutated inside the closure is declared in a _NamedMoveBinding_, | ||
the `IdentifierPattern` that declares the binding must have the `mut` keyword. | ||
|
||
If it is declared in an _UnnamedMoveBinding_, | ||
the `mut` keyword must be added in front of the expression; | ||
since the declared binding is always the first token in the expression, | ||
the `mut` token is always immediately followed by the mutable binding, | ||
thus yielding consistent readability. | ||
|
||
If it is implicitly captured from the parent scope | ||
instead of declared in a _MoveBinding_, | ||
the local variable declaration must be declared `mut` too. | ||
|
||
## _ImplicitMove_ closures | ||
|
||
When the `move` keyword is present but _MoveBindings_ is absent (with its parentheses absent as well), | ||
the closure expression is of the _ImplicitMove_ type, where | ||
all local variables in the closure construction scope | ||
are implicitly moved or copied into the closure on demand. | ||
|
||
Note that `move` with an empty pair of parentheses is allowed and follows the former rule; | ||
in other words, `move() |...| {...}` and `|...| {...}` are semantically equivalent. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ughh, I have to admit to finding this rather counter-intuitive: |
||
This allows macros to emit repeating groups of `_MoveBinding_ ","` inside a pair of parentheses | ||
and achieve correct semantics when there are zero repeating groups. | ||
|
||
If a moved binding is mutated inside the closure, | ||
its declaration in the parent scope must be declared `mut` too. | ||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
Due to backwards compatibility, this RFC proposes a new syntax | ||
that is an extension of capture-by-move | ||
but actually looks more similar to capture-by-reference, | ||
thus confusing new users. | ||
|
||
# Rationale and alternatives | ||
[rationale-and-alternatives]: #rationale-and-alternatives | ||
|
||
Capture-by-reference is the default behavior for implicit captures for two reasons: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to suggest the opposite, here, actually: for
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think, rather than adding |
||
|
||
1. It is more consistent to have `move(x)` imply `move(x=x)`, | ||
which leaves us with implicit references for the unspecified. | ||
Comment on lines
+205
to
+206
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that writing |
||
2. Move bindings actually define a new shadowing binding | ||
that is completely independent of the original binding, | ||
so it is more correct to have the new binding explicitly named. | ||
Consider how unintuitive it is to require that | ||
a moved variable be declared `mut` in the outer scope | ||
even though it is only mutated inside the closure (as the new binding). | ||
Comment on lines
+207
to
+212
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once this RFC lands, I could imagine there being a lint against implicitly by- |
||
|
||
The possible syntax for automatically-inferred _MoveBinding_ pattern | ||
is strictly limited to allow maximum future compatibility. | ||
Currently, many cases of captured bindings are in the form of | ||
`foo = foo`, `foo = &foo` or `foo.clone()`. | ||
This RFC intends to solve the ergonomic issues for these common scenarios first | ||
and leave more room for future enhancement when other frequent patterns are identified. | ||
|
||
Alternative approaches previously proposed | ||
include explicitly adding support for the `clone` keyword. | ||
This RFC does not favor such suggestions | ||
as they make the fundamental closure expression syntax | ||
unnecessarily dependent on the `clone` language item, | ||
and does not offer possibilities for alternative transformers. | ||
|
||
SOF3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# Prior art | ||
[prior-art]: #prior-art | ||
|
||
## Other languages | ||
|
||
Closure expressions (with the ability to capture) are known to many languages, | ||
varying between explicit and implicit capturing. | ||
Nevertheless, most such languages do not support capturing by reference. | ||
Examples of languages that support capture-by-reference include | ||
C++ lambdas (`[x=f(y)]`) and PHP (`use(&$x)`). | ||
Of these, C++ uses a leading `&`/`=` in the capture list | ||
to indicate the default behavior as move or reference, | ||
and allows an initializer behind a variable: | ||
|
||
```cpp | ||
int foo = 1; | ||
auto closure = [foo = foo+1]() mutable { | ||
foo += 10; // does not mutate ::foo | ||
return foo; | ||
} | ||
closure(); // 12 | ||
closure(); // 22 | ||
``` | ||
|
||
This RFC additionally proposes the ability to omit the capture identifier, | ||
because use cases of `foo.clone()` are much more common in Rust, | ||
compared to C++ where most values may be implicitly cloned. | ||
|
||
## Rust libraries | ||
|
||
Attempts to improve ergonomics for cloning into closures were seen in proc macros: | ||
|
||
- [enclose](https://crates.io/crates/enclose) | ||
- [clown](https://crates.io/crates/clown) | ||
- [closet](https://crates.io/crates/closet) | ||
- [capture](https://crates.io/crates/capture) | ||
|
||
# Unresolved questions | ||
[unresolved-questions]: #unresolved-questions | ||
|
||
- This RFC actually solves two not necessarily related problems together, | ||
namely clone-into-closures and selective capture-by-move. | ||
It might be more appropriate to split the former to a separate RFC, | ||
but they are currently put together such that | ||
consideration for the new syntax includes possibility for both enhancements. | ||
|
||
# Future possibilities | ||
[future-possibilities]: #future-possibilities | ||
|
||
- Should we consider deprecating the _ImplicitMove_ syntax | ||
in favor of explicitly specifying what gets moved, | ||
especially for mutable variables, | ||
considering that moved variables actually create a new, shadowing binding? | ||
- The set of allowed expressions may be extended in the future. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might be missing some implied boilerplate here, but is this just two lines?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could be just one line (and maybe even inlined in another expression). So that's 3.5 lines of boilerplate.