From 3c2d8b62f7cfce47fad4d5665f10ad982860590f Mon Sep 17 00:00:00 2001 From: SOFe Date: Tue, 10 Oct 2023 05:29:18 +0000 Subject: [PATCH 1/8] Add closure-move-bindings RFC --- text/3512-closure-move-bindings.md | 285 +++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 text/3512-closure-move-bindings.md diff --git a/text/3512-closure-move-bindings.md b/text/3512-closure-move-bindings.md new file mode 100644 index 00000000000..28698a9a42f --- /dev/null +++ b/text/3512-closure-move-bindings.md @@ -0,0 +1,285 @@ +- 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 = || { 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. +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) || { + 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: + +> **Syntax**\ +> _ClosureExpression_ :\ +>    ( `move` _MoveBindings_? )?\ +>    ( `||` | `|` _ClosureParameters_? `|` )\ +>    (_Expression_ | `->` _TypeNoBounds_ _BlockExpression_)>\ +> _MoveBindings_ :\ +>    `(` ( _MoveBinding_ (`,` _MoveBinding_)\* `,`? )? `)`\ +> _MoveBinding_ :\ +>    _NamedMoveBinding_ | _UnnamedMoveBinding_\ +> _NamedMoveBinding_ :\ +>    _PatternNoTopAlt_ `=` _Expression_\ +> _UnnamedMoveBinding_ :\ +>    `mut`? ( _IdentifierExpression_ | _MethodCallExpression_ )\ +>    ( _Identifier_ `=` | `mut`)? ( _IdentifierExpression_ | _MethodCallExpression_ )\ +> _ClosureParameters_ :\ +>    _ClosureParam_ (`,` _ClosureParam_)\* `,`?\ +> _ClosureParam_ :\ +>    _OuterAttribute_\* _PatternNoTopAlt_ ( `:` _Type_ )? + +Closure expressions are clsasified into two main types, +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. + +## _ImplicitReference_ 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. +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: + +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. + +# 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. From d8a146140bd5a556ae7b053552d26fc28c92a5a8 Mon Sep 17 00:00:00 2001 From: Jonathan Chan Kwan Yin Date: Wed, 11 Oct 2023 08:55:04 +0800 Subject: [PATCH 2/8] Update 3512-closure-move-bindings.md --- text/3512-closure-move-bindings.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/text/3512-closure-move-bindings.md b/text/3512-closure-move-bindings.md index 28698a9a42f..a298e1c87e8 100644 --- a/text/3512-closure-move-bindings.md +++ b/text/3512-closure-move-bindings.md @@ -131,7 +131,6 @@ A closure expression has the following syntax: >    _PatternNoTopAlt_ `=` _Expression_\ > _UnnamedMoveBinding_ :\ >    `mut`? ( _IdentifierExpression_ | _MethodCallExpression_ )\ ->    ( _Identifier_ `=` | `mut`)? ( _IdentifierExpression_ | _MethodCallExpression_ )\ > _ClosureParameters_ :\ >    _ClosureParam_ (`,` _ClosureParam_)\* `,`?\ > _ClosureParam_ :\ @@ -178,7 +177,7 @@ If it is implicitly captured from the parent scope instead of declared in a _MoveBinding_, the local variable declaration must be declared `mut` too. -## _ImplicitReference_ closures +## _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 From 968b93e0f9cedaa42a2c95f977cc0b5a0701ea8a Mon Sep 17 00:00:00 2001 From: Jonathan Chan Kwan Yin Date: Wed, 11 Oct 2023 08:55:40 +0800 Subject: [PATCH 3/8] Update text/3512-closure-move-bindings.md Co-authored-by: Jacob Lifshay --- text/3512-closure-move-bindings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/3512-closure-move-bindings.md b/text/3512-closure-move-bindings.md index a298e1c87e8..648a25b2812 100644 --- a/text/3512-closure-move-bindings.md +++ b/text/3512-closure-move-bindings.md @@ -59,7 +59,7 @@ instead of referenced: ```rs let mut foo = 1; -let mut closure = || { foo = 2; }; +let mut closure = move || { foo = 2; }; closure(); dbg!(foo); // foo is still 1, but the copy of `foo` in `closure` is 2 ``` From ea48e5df5877dc17d06a04711d1ff0c0012a9383 Mon Sep 17 00:00:00 2001 From: Jonathan Chan Kwan Yin Date: Wed, 11 Oct 2023 15:13:56 +0800 Subject: [PATCH 4/8] Update text/3512-closure-move-bindings.md Co-authored-by: Lukas Wirth --- text/3512-closure-move-bindings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/3512-closure-move-bindings.md b/text/3512-closure-move-bindings.md index 648a25b2812..6ed53c894bd 100644 --- a/text/3512-closure-move-bindings.md +++ b/text/3512-closure-move-bindings.md @@ -68,7 +68,7 @@ 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. +all the `move` keyword makes them all captured by moving. To move only specific bindings, list them in parentheses after `move`: From 7226c61477bf7209d8ecf8f5aeac34e785217717 Mon Sep 17 00:00:00 2001 From: Jonathan Chan Kwan Yin Date: Wed, 11 Oct 2023 15:14:03 +0800 Subject: [PATCH 5/8] Update text/3512-closure-move-bindings.md Co-authored-by: Lukas Wirth --- text/3512-closure-move-bindings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/3512-closure-move-bindings.md b/text/3512-closure-move-bindings.md index 6ed53c894bd..d264b432a45 100644 --- a/text/3512-closure-move-bindings.md +++ b/text/3512-closure-move-bindings.md @@ -136,7 +136,7 @@ A closure expression has the following syntax: > _ClosureParam_ :\ >    _OuterAttribute_\* _PatternNoTopAlt_ ( `:` _Type_ )? -Closure expressions are clsasified into two main types, +Closure expressions are classified into two main types, namely _ImplicitReference_ and _ImplicitMove_. A closure expression is _ImplicitMove_ IF AND ONLY IF it starts with a `move` token immediately followed by a `|` token, From c3d152a3200bd18fcf439f32c769992d554bc06a Mon Sep 17 00:00:00 2001 From: Jonathan Chan Kwan Yin Date: Thu, 12 Oct 2023 18:24:56 +0800 Subject: [PATCH 6/8] Update text/3512-closure-move-bindings.md Co-authored-by: teor --- text/3512-closure-move-bindings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/3512-closure-move-bindings.md b/text/3512-closure-move-bindings.md index d264b432a45..bb5781a8fbc 100644 --- a/text/3512-closure-move-bindings.md +++ b/text/3512-closure-move-bindings.md @@ -68,7 +68,7 @@ Note that `foo` is _copied_ during move in this example as `i32` implements `Copy`. If a closure captures multiple bindings, -all the `move` keyword makes them all captured by moving. +the `move` keyword makes them all captured by moving. To move only specific bindings, list them in parentheses after `move`: From 5c384561acee244293eb10be1a3be632d490f8a3 Mon Sep 17 00:00:00 2001 From: SOFe Date: Sun, 19 Nov 2023 21:40:08 +0800 Subject: [PATCH 7/8] Correct move details about ByUsage closures, remove MethodCallExpression shorthand --- text/3512-closure-move-bindings.md | 47 ++++++++++++++---------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/text/3512-closure-move-bindings.md b/text/3512-closure-move-bindings.md index bb5781a8fbc..5185a76cdf7 100644 --- a/text/3512-closure-move-bindings.md +++ b/text/3512-closure-move-bindings.md @@ -44,7 +44,8 @@ This RFC proposes a more concise syntax to express these moving semantics. [guide-level-explanation]: #guide-level-explanation A closure may capture bindings in its defining scope. -Bindings are captured by reference by default: +By default, bindings are captured by usage, +i.e. by the first possible of shared reference, mutable reference or move. ```rs let mut foo = 1; @@ -54,8 +55,8 @@ 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: +to indicate that all captured bindings are always moved into the closure, +useful for avoiding references to local variables: ```rs let mut foo = 1; @@ -69,7 +70,7 @@ as `i32` implements `Copy`. If a closure captures multiple bindings, the `move` keyword makes them all captured by moving. -To move only specific bindings, +To only indicate this for specific bindings, list them in parentheses after `move`: ```rs @@ -85,6 +86,7 @@ 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: @@ -109,10 +111,6 @@ 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 @@ -130,37 +128,36 @@ A closure expression has the following syntax: > _NamedMoveBinding_ :\ >    _PatternNoTopAlt_ `=` _Expression_\ > _UnnamedMoveBinding_ :\ ->    `mut`? ( _IdentifierExpression_ | _MethodCallExpression_ )\ +>    `mut`? _IdentifierExpression_ \ > _ClosureParameters_ :\ >    _ClosureParam_ (`,` _ClosureParam_)\* `,`?\ > _ClosureParam_ :\ >    _OuterAttribute_\* _PatternNoTopAlt_ ( `:` _Type_ )? Closure expressions are classified into two main types, -namely _ImplicitReference_ and _ImplicitMove_. -A closure expression is _ImplicitMove_ IF AND ONLY IF +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. -## _ImplicitReference_ closures +## _ByUsage_ closures -When the parentheses for _MoveBindings_ is present, or when the `move` keyword is absent, -the closure expression is of the _ImplicitReference_ type, where +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 or mutable reference on demand, -preferring shared reference if possible. +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 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. +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 @@ -177,10 +174,10 @@ 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 +## _FullMove_ 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 +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. @@ -277,7 +274,7 @@ consideration for the new syntax includes possibility for both enhancements. # Future possibilities [future-possibilities]: #future-possibilities -- Should we consider deprecating the _ImplicitMove_ syntax +- 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? From 70a14b35835aafe9080a913cdf845f7fa0ab8788 Mon Sep 17 00:00:00 2001 From: Jonathan Chan Kwan Yin Date: Mon, 18 Dec 2023 10:46:42 +0800 Subject: [PATCH 8/8] Update text/3512-closure-move-bindings.md Co-authored-by: Josh Triplett --- text/3512-closure-move-bindings.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/text/3512-closure-move-bindings.md b/text/3512-closure-move-bindings.md index 5185a76cdf7..0f4313246a2 100644 --- a/text/3512-closure-move-bindings.md +++ b/text/3512-closure-move-bindings.md @@ -225,6 +225,13 @@ 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 [prior-art]: #prior-art