Adds the syntax move(bindings) |...| ...
to explicitly specify how to capture bindings into a closure.
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:
- 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:
This RFC proposes a more concise syntax to express these moving semantics.
Guide-level explanation
A closure may capture bindings in its defining scope. By default, bindings are captured by usage, i.e. by the first possible of shared reference, mutable reference or move.
let mut foo = 1;
let mut closure = ;
closure;
dbg!; // foo is now 2
You can add a move keyword in front of the closure
to indicate that all captured bindings are always moved into the closure,
useful for avoiding references to local variables:
let mut foo = 1;
let mut closure = move || ;
closure;
dbg!; // 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,
the move keyword makes them all captured by moving.
To only indicate this for specific bindings,
list them in parentheses after move:
let foo = 1;
let mut bar = 2;
let mut closure = move || ;
closure;
dbg!; // 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.
Meanwhile, bar continues to capture by usage (i.e. by reference).
Moved bindings may also be renamed:
let mut foo = 1;
let mut closure = move || ;
closure;
dbg!; // the outer `foo` is 2 as it was captured by reference
Bindings may be transformed when moved:
let foo = vec!;
let mut closure = move || ;
closure;
dbg!; // the outer `foo` is still [1] because only the cloned copy was mutated
Reference-level explanation
A closure expression has the following syntax:
Syntax
ClosureExpression :
(moveMoveBindings? )?
(||||ClosureParameters?|)
(Expression |->TypeNoBounds BlockExpression)>
MoveBindings :
(( MoveBinding (,MoveBinding)*,? )?)
MoveBinding :
NamedMoveBinding | UnnamedMoveBinding
NamedMoveBinding :
PatternNoTopAlt=Expression
UnnamedMoveBinding :
mut? IdentifierExpression
ClosureParameters :
ClosureParam (,ClosureParam)*,?
ClosureParam :
OuterAttribute* PatternNoTopAlt (:Type )?
Closure expressions are classified into two main types,
namely ByUsage and FullMove.
A closure expression is FullMove IF AND ONLY IF
it starts with a move token immediately followed by a | token,
without any parentheses in between.
ByUsage closures
When the parentheses for MoveBindings is present,
or when the move keyword is absent,
the closure expression is of the ByUsage type, where
all local variables in the closure construction scope not shadowed by any MoveBinding
are implicitly captured into the closure
by shared reference, mutable reference or move on demand,
preferring the first possible type.
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 a single-segment (identifier) PathExpression.
The left-side pattern is then automatically inferred to be a IdentifierPattern
using the identifier 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.
FullMove closures
When the move keyword is present but MoveBindings is absent (with its parentheses absent as well),
the closure expression is of the FullMove 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.
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
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
Capture-by-reference is the default behavior for implicit captures for two reasons:
- It is more consistent to have
move(x)implymove(x=x), which leaves us with implicit references for the unspecified. - 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
mutin the outer scope even though it is only mutated inside the closure (as the new binding).
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.
move inside parameter list
As an alternative to having move(binding) ahead of a closure, we could put move binding in the parameter list: |x: T, move y, move z = z.clone()| { ... }.
This would have the advantage of keeping the list of declared names in one place, and giving a view of closures as having some bindings passed in as arguments and other bindings captured from the containing scope.
However, this would have the disadvantage of visually looking like the caller could pass the value in as a parameter, which it cannot.
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:
int foo = 1;
auto closure = mutable
; // 12
; // 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:
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
- Should we consider deprecating the FullMove 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.