Capture Clauses as Effects (Rust)

dabinat1 pts0 comments

Capture Clauses as Effects — Yosh Wuyts — Blog

Author<br>Yosh Wuyts

Published on<br>2026-07-15

Samples licensed as<br>Apache 2.0

Citation

Capture Clauses as Effects

Introduction<br>In my post on Hoisting<br>Expressions I discussed the<br>move($expr) feature and how it works much like an inverse of the defer<br>feature many languages have. Instead of creating expressions which run after<br>the scope ends, move($expr) runs code before the scope is entered<br>1. The goal of this code is to make it so clone-heavy ecosystems<br>like bevy have a much better experience using Rust.

1Actually it’s run at closure instantiation time, but that’s also before from the perspective of the closure body.

Even though I agree with the goals, I’m less sure about the proposal. I feel<br>that explicit capture<br>clauses<br>provide a simpler model and are therefore preferable. But they have the<br>downside they can feel a bit clunky to write and can break up the writing flow.<br>And so in this post I want to explore a potential solution here by making the<br>conversion of references to owned values a first-class language feature.

Setting the stage<br>Let’s start by using a simplified version of the motivating example shown in<br>RFC 3968. This here is a typical task::spawn example you’ll see in a lot of async Rust programs 1. It clones some fields out of a struct, moves those into the closure, and passes those to another function:

1That’s not a good thing, but I digress.

rust<br>let a = foo.a.clone();<br>let b = foo.b.clone();<br>let c = foo.c.clone();<br>task::spawn(async move {<br>bar(a, b, c).await<br>});Copy<br>The problem with this code is that it is a fair bit of ceremony. Variable<br>shadowing can sometimes create problems with naming, each clone lives on its own<br>line, and when writing closure code you’ll often find yourself jumping back and<br>forth to make sure you’re cloning the right variables. If we want to improve the<br>ergonomics of cloning this is what we’re trying to improve upon.

A menagerie of moves<br>There are different kinds of closure captures, and for any proposed language<br>feature it’s important to cover a full range of examples. Let’s walk through a<br>number of them so we can refer to them later as we start working through a design:

Example 1: move three variables<br>The variables a, b, and c all directly moved into the closure.

rust<br>bar(move || {<br>baz(a, b, c);<br>});Copy<br>Example 2: clone three variables<br>The variables a, b, and c all cloned before being moved into the closure.

rust<br>let a = a.clone();<br>let b = b.clone();<br>let c = c.clone();

bar(move || {<br>baz(a, b, c);<br>});Copy<br>Example 3: move one variable, clone two<br>Here we clone b, and c before moving them, but move a directly without cloning.

rust<br>let b = b.clone();<br>let c = c.clone();

bar(move || {<br>baz(a, b, c);<br>});Copy<br>Example 4: reference one variable, clone two<br>Here we clone b, and c before moving them, but move a directly without cloning.

rust<br>let b = b.clone();<br>let c = c.clone();

bar(move || {<br>baz(&a, b, c);<br>});Copy<br>Example 5: clone three fields<br>Here we have a struct foo which contains three fields a, b, and c which<br>we all clone before moving.

rust<br>let a = foo.a.clone();<br>let b = foo.b.clone();<br>let c = foo.c.clone();

bar(move || {<br>baz(a, b, c);<br>});Copy<br>Example 6: clone two fields, keep one<br>Here we have a struct foo which contains three fields a, b, and c. We only clone b and c before moving. We do not move a.

rust<br>let b = foo.b.clone();<br>let c = foo.c.clone();

bar(move || {<br>baz(b, c);<br>});Copy<br>Example 7: clone two fields, move one<br>Here we have a struct foo which contains three fields a, b, and c. We only clone b and c before moving. We move a without cloning it.

rust<br>let a = foo.a;<br>let b = foo.b.clone();<br>let c = foo.c.clone();

bar(move || {<br>baz(a, b, c);<br>});Copy<br>Example 8: clone two fields, reference one<br>Here we have a struct foo which contains three fields a, b, and c. We only clone b and c before moving. We pass a by reference.

rust<br>let b = foo.b.clone();<br>let c = foo.c.clone();

bar(move || {<br>baz(&foo.a, b, c);<br>});Copy<br>Example 9: move three fields<br>Here we have a struct foo which contains three fields a, b, and c. We<br>move all three fields into the closure.

rust<br>let b = foo.a;<br>let b = foo.b;<br>let c = foo.c;

bar(move || {<br>baz(a, b, c);<br>});Copy<br>Example 10: move one field, clone one, reference one<br>Here we have a struct foo which contains three fields a, b, and c. We move a, clone b, and reference c.

rust<br>let b = foo.a;<br>let b = foo.b.clone();<br>let c = &foo.c;

bar(move || {<br>baz(a, b, c);<br>});Copy<br>Explicit closure captures, round one<br>In Explicit Capture<br>Clauses<br>Niko Matsakis shows off what looks like a rather pleasant notation for explicit<br>closure captures. It is based on field access in the form of move(a.b.c) to<br>get a named variable c. With that notation example 9 (move three) would look<br>like this:

rust<br>bar(move(foo.a, foo.b, foo.c) || {<br>baz(a, b, c);<br>});Copy<br>Niko envisions this as a short-hand notation for more general place = expression pairs. The example above would just be sugar for the following:

rust<br>bar(move(<br>foo.a...

clone move rust example fields copy

Related Articles