← Back to index
PR #3512Work-in-progress preview from an open pull request.View on GitHub ↗
REVIEW
#3512

closure-move-bindings

AuthorSOF3
CreatedOct 9 2023
UpdatedMay 21 2025
Rust Issue

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:
{
    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:
{
    let foo = foo.clone();
    move || foo.run()
}

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 = || { 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 always moved into the closure, useful for avoiding references to local variables:

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, 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(mut foo) || {
    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. 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(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:

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

Reference-level explanation

A closure expression has the following syntax:

Syntax
ClosureExpression :
   ( move MoveBindings? )?
   ( || | | 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:

  1. It is more consistent to have move(x) imply move(x=x), which leaves us with implicit references for the unspecified.
  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).

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 = [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:

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.