Let's be realistic, in the real world the variables won't be called with one-letter names a
, b
, c
, and there can be more captures, so your proposal is much more verbose than you make it look. Also, the explicit capture needs to be introduced not just for closures, but also for async {}
blocks. In fact, the latter are a much worse ergonomic offender, since their autocapture rules are somewhy more strict and different from closures' ones. Finally, remember that async closures don't exist yet, you need to return an async {}
from a closure. This means you must nest your explicit captures.
Here is an only slightly cherry-picked realistic example, from the Zed editor source code (crates/assistant/src/assistant_panel.rs::open_context):
cx.spawn(closure!(
move saved_context, lsp_adapter_delegate;
clone path, self.languages, self.telemetry, self.slash_commands;
workspace;
|this, mut cx| async_block!(
mut cx;
move this, saved_context, lsp_adapter_delegate;
clone path, self.languages, self.telemetry, self.slash_commands;
workspace;
async move {
/* body */
}
)
))
I don't think anyone would want to write or read this. Frankly, it's worse than the status quo, even more verbose and hard to get right. And if I modify the body, I need to carefully modify twice the explicit captures as well?
Actually, your syntax doesn't allow to capture fields, e.g. self.slash_commands
. There's no (and probably shouldn't be) a rule for implicit introduction of bindings based on expressions in the capture list. This means that those fields must be manually cloned above, with explicit new bindings, and then those bindings need to be explicitly moved, twice.
Frankly, I don't know why you would even include mandatory capture lists in your proposal. It seems that the current autocapture rules are perfectly fine for the vast majority of captures, and one only need explicit capture mode listed for a few specific variables which aren't captured in the desired way. That would eliminate much of the boilerplate in your proposal without requiring any changes to existing code.
I see the following issues with the explicit capture approach in general:
- The duplication of captures for nested closures and async blocks, as noted above. This will also get worse if Rust adds more types of blocks, like generators.
- Capturing expressions, including very trivial ones like field accesses, requires a syntax for explicit introduction of new bindings. It's more verbose and becomes even less of an ergonomic win over the current approach (nested blocks with explicit bindings). But if the syntax is restricted only to simple binding names, like for string and macro interpolation, then much of the benefit is lost, since it's quite common to capture specific fields in methods, like in the example above.
- The simple form of "move this, clone that" doesn't have support for more complex operations than mere
Clone::clone
call. I'd very much want to see support for custom allocators stabilized, but such collections would need to take the allocator as explicit parameter for cloning (at least in certain approaches). There are also cases like DynClone
, where you want to clone a value of T
from an erased trait object Box<dyn Foo>
: you add a separate trait DynClone
, Foo: DynClone
, and that trait has methods to clone trait objects. Granted, those are a bit of niche cases, but Rust has historically been great at providing first-class support for usually neglected niche cases, like embedded and low-level development.