Skip to content

Commit 8edcf1e

Browse files
committed
Add closure-move-bindings RFC
1 parent e49fd46 commit 8edcf1e

File tree

1 file changed

+285
-0
lines changed

1 file changed

+285
-0
lines changed

text/3512-closure-move-bindings.md

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
- Feature `closure_move_bindings`
2+
- Start Date: 2023-10-09
3+
- RFC PR: [rust-lang/rfcs#3512](https://github.com/rust-lang/rfcs/pull/3512)
4+
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
Adds the syntax `move(bindings) |...| ...`
10+
to explicitly specify how to capture bindings into a closure.
11+
12+
# Motivation
13+
[motivation]: #motivation
14+
15+
Currently there are two ways to capture local bindings into a closure,
16+
namely by reference (`|| foo`) and by moving (`move || foo`).
17+
This mechanism has several ergonomic problems:
18+
19+
- It is not possible to move some bindings and reference the others.
20+
To do so, one must define another binding that borrows the value
21+
and move it into the closure:
22+
23+
```rs
24+
{
25+
let foo = &foo;
26+
move || run(foo, bar)
27+
}
28+
```
29+
30+
- It is a very frequent scenario to clone a value into a closure
31+
(especially common with `Rc`/`Arc`-based values),
32+
but even the simplest scenario requires three lines of boilerplate:
33+
34+
```rs
35+
{
36+
let foo = foo.clone();
37+
move || foo.run()
38+
}
39+
```
40+
41+
This RFC proposes a more concise syntax to express these moving semantics.
42+
43+
# Guide-level explanation
44+
[guide-level-explanation]: #guide-level-explanation
45+
46+
A closure may capture bindings in its defining scope.
47+
Bindings are captured by reference by default:
48+
49+
```rs
50+
let mut foo = 1;
51+
let mut closure = || { foo = 2; };
52+
closure();
53+
dbg!(foo); // foo is now 2
54+
```
55+
56+
You can add a `move` keyword in front of the closure
57+
to indicate that all captured bindings are moved into the closure
58+
instead of referenced:
59+
60+
```rs
61+
let mut foo = 1;
62+
let mut closure = || { foo = 2; };
63+
closure();
64+
dbg!(foo); // foo is still 1, but the copy of `foo` in `closure` is 2
65+
```
66+
67+
Note that `foo` is *copied* during move in this example
68+
as `i32` implements `Copy`.
69+
70+
If a closure captures multiple bindings,
71+
all the `move` keywoed makes them all captured by moving.
72+
To move only specific bindings,
73+
list them in parentheses after `move`:
74+
75+
```rs
76+
let foo = 1;
77+
let mut bar = 2;
78+
let mut closure = move(mut foo) || {
79+
foo += 10;
80+
bar += 10;
81+
};
82+
closure();
83+
dbg!(foo, bar); // foo = 1, bar = 12
84+
```
85+
86+
Note that the outer `foo` no longer requires `mut`;
87+
it is relocated to the closure since it defines a new binding.
88+
89+
Moved bindings may also be renamed:
90+
91+
```rs
92+
let mut foo = 1;
93+
let mut closure = move(mut bar = foo) || {
94+
foo = 2;
95+
bar = 3;
96+
};
97+
closure();
98+
dbg!(foo); // the outer `foo` is 2 as it was captured by reference
99+
```
100+
101+
Bindings may be transformed when moved:
102+
103+
```rs
104+
let foo = vec![1];
105+
let mut closure = move(mut foo = foo.clone()) || {
106+
foo.push(2);
107+
};
108+
closure();
109+
dbg!(foo); // the outer `foo` is still [1] because only the cloned copy was mutated
110+
```
111+
112+
The above may be simplified to `move(mut foo.clone())` as well.
113+
This simplification is only allowed
114+
when the transformation expression is a method call on the captured binding.
115+
116+
# Reference-level explanation
117+
[reference-level-explanation]: #reference-level-explanation
118+
119+
A closure expression has the following syntax:
120+
121+
> **<sup>Syntax</sup>**\
122+
> _ClosureExpression_ :\
123+
> &nbsp;&nbsp; ( `move` _MoveBindings_<sup>?</sup> )<sup>?</sup>\
124+
> &nbsp;&nbsp; ( `||` | `|` _ClosureParameters_<sup>?</sup> `|` )\
125+
> &nbsp;&nbsp; (_Expression_ | `->` _TypeNoBounds_&nbsp;_BlockExpression_)>
126+
> _MoveBindings_ :\
127+
> &nbsp;&nbsp; `(` (_MoveBinding_ `,`)<sup>\*</sup> `)`
128+
> _MoveBinding_ :\
129+
> &nbsp;&nbsp; _NamedMoveBinding_ | _UnnamedMoveBinding_
130+
> _NamedMoveBinding_ :\
131+
> &nbsp;&nbsp; _PatternNoTopAlt_ `=` _Expression_
132+
> _UnnamedMoveBinding_ :\
133+
> &nbsp;&nbsp; `mut`<sup>?</sup> ( _IdentifierExpression_ | _MethodCallExpression_ )
134+
> &nbsp;&nbsp; ( _Identifier_ `=` | `mut`)<sup>?</sup> ( _IdentifierExpression_ | _MethodCallExpression_ )
135+
> _ClosureParameters_ :\
136+
> &nbsp;&nbsp; _ClosureParam_ (`,` _ClosureParam_)<sup>\*</sup> `,`<sup>?</sup>
137+
> _ClosureParam_ :\
138+
> &nbsp;&nbsp; _OuterAttribute_<sup>\*</sup> _PatternNoTopAlt_&nbsp;( `:` _Type_ )<sup>?</sup>
139+
140+
Closure expressions are clsasified into two main types,
141+
namely _ImplicitReference_ and _ImplicitMove_.
142+
A closure expression is _ImplicitMove_ IF AND ONLY IF
143+
it starts with a `move` token immediately followed by a `|` token,
144+
without any parentheses in between.
145+
146+
## _ImplicitReference_ closures
147+
148+
When the parentheses for _MoveBindings_ is present, or when the `move` keyword is absent,
149+
the closure expression is of the _ImplicitReference_ type, where
150+
all local variables in the closure construction scope not shadowed by any _MoveBinding_
151+
are implicitly captured into the closure by shared or mutable reference on demand,
152+
preferring shared reference if possible.
153+
154+
Each _MoveBinding_ declares binding(s) in its left-side pattern,
155+
assigned with the value of the right-side expression evaluated during closure construction,
156+
thus referencing any relevant local variables if necessary.
157+
158+
If the left-side pattern is omitted (_UnnamedMoveBinding_),
159+
the expression must be either a single-segment (identifier) `PathExpression`
160+
or a _MethodCallExpression_,
161+
the receiver expression of which must be a single identifier variable,
162+
and the argument list must not reference any local variables.
163+
The left-side pattern is then automatically inferred to be a simple _IdentifierPattern_
164+
using the identifier/receiver as the new binding.
165+
166+
### Mutable bindings
167+
168+
If a captured binding mutated inside the closure is declared in a _NamedMoveBinding_,
169+
the `IdentifierPattern` that declares the binding must have the `mut` keyword.
170+
171+
If it is declared in an _UnnamedMoveBinding_,
172+
the `mut` keyword must be added in front of the expression;
173+
since the declared binding is always the first token in the expression,
174+
the `mut` token is always immediately followed by the mutable binding,
175+
thus yielding consistent readability.
176+
177+
If it is implicitly captured from the parent scope
178+
instead of declared in a _MoveBinding_,
179+
the local variable declaration must be declared `mut` too.
180+
181+
## _ImplicitReference_ closures
182+
183+
When the `move` keyword is present but _MoveBindings_ is absent (with its parentheses absent as well),
184+
the closure expression is of the _ImplicitMove_ type, where
185+
all local variables in the closure construction scope
186+
are implicitly moved or copied into the closure on demand.
187+
188+
Note that `move` with an empty pair of parentheses is allowed and follows the former rule;
189+
in other words, `move() |...| {...}` and `|...| {...}` are semantically equivalent.
190+
This allows macros to emit repeating groups of `_MoveBinding_ ","` inside a pair of parentheses
191+
and achieve correct semantics when there are zero repeating groups.
192+
193+
If a moved binding is mutated inside the closure,
194+
its declaration in the parent scope must be declared `mut` too.
195+
196+
# Drawbacks
197+
[drawbacks]: #drawbacks
198+
199+
Due to backwards compatibility, this RFC proposes a new syntax
200+
that is an extension of capture-by-move
201+
but actually looks more similar to capture-by-reference,
202+
thus confusing new users.
203+
204+
# Rationale and alternatives
205+
[rationale-and-alternatives]: #rationale-and-alternatives
206+
207+
Capture-by-reference is the default behavior for implicit captures for two reasons:
208+
209+
1. It is more consistent to have `move(x)` imply `move(x=x)`,
210+
which leaves us with implicit references for the unspecified.
211+
2. Move bindings actually define a new shadowing binding
212+
that is completely independent of the original binding,
213+
so it is more correct to have the new binding explicitly named.
214+
Consider how unintuitive it is to require that
215+
a moved variable be declared `mut` in the outer scope
216+
even though it is only mutated inside the closure (as the new binding).
217+
218+
The possible syntax for automatically-inferred _MoveBinding_ pattern
219+
is strictly limited to allow maximum future compatibility.
220+
Currently, many cases of captured bindings are in the form of
221+
`foo = foo`, `foo = &foo` or `foo.clone()`.
222+
This RFC intends to solve the ergonomic issues for these common scenarios first
223+
and leave more room for future enhancement when other frequent patterns are identified.
224+
225+
Alternative approaches previously proposed
226+
include explicitly adding support for the `clone` keyword.
227+
This RFC does not favor such suggestions
228+
as they make the fundamental closure expression syntax
229+
unnecessarily dependent on the `clone` language item,
230+
and does not offer possibilities for alternative transformers.
231+
232+
# Prior art
233+
[prior-art]: #prior-art
234+
235+
## Other languages
236+
237+
Closure expressions (with the ability to capture) are known to many languages,
238+
varying between explicit and implicit capturing.
239+
Nevertheless, most such languages do not support capturing by reference.
240+
Examples of languages that support capture-by-reference include
241+
C++ lambdas (`[x=f(y)]`) and PHP (`use(&$x)`).
242+
Of these, C++ uses a leading `&`/`=` in the capture list
243+
to indicate the default behavior as move or reference,
244+
and allows an initializer behind a variable:
245+
246+
```cpp
247+
int foo = 1;
248+
auto closure = [foo = foo+1]() mutable {
249+
foo += 10; // does not mutate ::foo
250+
return foo;
251+
}
252+
closure(); // 12
253+
closure(); // 22
254+
```
255+
256+
This RFC additionally proposes the ability to omit the capture identifier,
257+
because use cases of `foo.clone()` are much more common in Rust,
258+
compared to C++ where most values may be implicitly cloned.
259+
260+
## Rust libraries
261+
262+
Attempts to improve ergonomics for cloning into closures were seen in proc macros:
263+
264+
- [enclose](https://crates.io/crates/enclose)
265+
- [clown](https://crates.io/crates/clown)
266+
- [closet](https://crates.io/crates/closet)
267+
- [capture](https://crates.io/crates/capture)
268+
269+
# Unresolved questions
270+
[unresolved-questions]: #unresolved-questions
271+
272+
- This RFC actually solves two not necessarily related problems together,
273+
namely clone-into-closures and selective capture-by-move.
274+
It might be more appropriate to split the former to a separate RFC,
275+
but they are currently put together such that
276+
consideration for the new syntax includes possibility for both enhancements.
277+
278+
# Future possibilities
279+
[future-possibilities]: #future-possibilities
280+
281+
- Should we consider deprecating the _ImplicitMove_ syntax
282+
in favor of explicitly specifying what gets moved,
283+
especially for mutable variables,
284+
considering that moved variables actually create a new, shadowing binding?
285+
- The set of allowed expressions may be extended in the future.

0 commit comments

Comments
 (0)