-
Notifications
You must be signed in to change notification settings - Fork 12.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Stabilize async closures (RFC 3668) #132706
Conversation
275f9d3
to
6c80519
Compare
This comment has been minimized.
This comment has been minimized.
6c80519
to
5031dfa
Compare
Some changes occurred to MIR optimizations cc @rust-lang/wg-mir-opt Some changes occurred in coverage tests. cc @Zalathar The Miri subtree was changed cc @rust-lang/miri Some changes occurred in tests/ui/sanitizer cc @rust-lang/project-exploit-mitigations, @rcvalle Some changes occurred in src/tools/clippy cc @rust-lang/clippy |
Oh but we've only just begun! |
☔ The latest upstream changes (presumably #132746) made this pull request unmergeable. Please resolve the merge conflicts. |
5031dfa
to
f97a235
Compare
This comment has been minimized.
This comment has been minimized.
f97a235
to
d39c26e
Compare
This comment has been minimized.
This comment has been minimized.
d39c26e
to
28c1200
Compare
@rfcbot fcp merge |
☔ The latest upstream changes (presumably #133089) made this pull request unmergeable. Please resolve the merge conflicts. |
3841a4e
to
99ff04d
Compare
99ff04d
to
7696a99
Compare
Since oli approved above, and ehuss is happy with the prelude rewording. @bors r=oli-obk rollup=never |
a1da8a8
to
5a1a5e8
Compare
@bors r=oli-obk |
☀️ Test successful - checks-actions |
Finished benchmarking commit (f4f0faf): comparison URL. Overall result: no relevant changes - no action needed@rustbot label: -perf-regression Instruction countThis benchmark run did not return any relevant results for this metric. Max RSS (memory usage)Results (primary -3.0%, secondary 3.2%)This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.
CyclesResults (primary 3.0%, secondary 2.6%)This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.
Binary sizeThis benchmark run did not return any relevant results for this metric. Bootstrap: 769.736s -> 771.968s (0.29%) |
Async Closures Stabilization Report
This report proposes the stabilization of
#![feature(async_closure)]
(RFC 3668). This is a long-awaited feature that increases the expressiveness of the Rust language and fills a pressing gap in the async ecosystem.Stabilization summary
async || {}
which return futures that can borrow from their captures and can be higher-ranked in their argument lifetimes.AsyncFn
family of traits, analogous to theFn
family.Motivation
Without this feature, users hit two major obstacles when writing async code that uses closures and
Fn
trait bounds:That is, for the first, we cannot write:
And for the second, we cannot write:
Async closures provide a first-class solution to these problems.
For further background, please refer to the motivation section of the RFC.
Major design decisions since RFC
The RFC had left open the question of whether we would spell the bounds syntax for async closures...
We've decided to spell this as
AsyncFn{,Mut,Once}
.The
Fn
family of traits is special in many ways. We had originally argued that, due to this specialness, that perhaps theasync Fn
syntax could be adopted without having to decide whether a generalasync Trait
mechanism would ever be adopted. However, concerns have been raised that we may not want to useasync Fn
syntax unless we would pursue more general trait modifiers. Since there remain substantial open questions on those -- and we don't want to rush any design work there -- it makes sense to ship this needed feature using theAsyncFn
-style bounds syntax.Since we would, in no case, be shipping a generalized trait modifier system anytime soon, we'll be continuing to see
AsyncFoo
traits appear across the ecosystem regardless. If we were to ever later ship some general mechanism, we could at that time manage the migration fromAsyncFn
toasync Fn
, just as we'd be enabling and managing the migration of many other traits.Note that, as specified in RFC 3668, the details of the
AsyncFn*
traits are not exposed and they can only be named via the "parentheses sugar". That is, we can writeT: AsyncFn() -> u8
but notT: AsyncFn<Output = u8>
.Unlike the
Fn
traits, we cannot project to theOutput
associated type of theAsyncFn
traits. That is, while we can write......we cannot write:
The choice of
AsyncFn{,Mut,Once}
bounds syntax obviates, for our purposes here, another question decided after that RFC, which was how to order bound modifiers such asfor<'a> async Fn()
.Other than answering the open question in the RFC on syntax, nothing has changed about the design of this feature between RFC 3668 and this stabilization.
What is stabilized
For those interested in the technical details, please see the dev guide section I authored.
Async closures
Other than in how they solve the problems described above, async closures act similarly to closures that return async blocks, and can have parts of their signatures specified:
When called, they return an anonymous future type corresponding to the (not-yet-executed) body of the closure. These can be awaited like any other future.
What distinguishes async closures is that, unlike closures that return async blocks, the futures returned from the async closure can capture state from the async closure. For example:
The async closure captures
vec
with some&'closure mut Vec<String>
which lives until the closure is dropped. Every call toclosure()
returns a future which reborrows that mutable reference&'call mut Vec<String>
which lives until the future is dropped (e.g. it isawait
ed).As another example:
The closure is marked with
move
, which means it takes ownership of the string by value. The future that is returned by callingclosure()
returns a future which borrows a reference&'call String
which lives until the future is dropped (e.g. it isawait
ed).Async fn trait family
To support the lending capability of async closures, and to provide a first-class way to express higher-ranked async closures, we introduce the
AsyncFn*
family of traits. See the corresponding section of the RFC.We stabilize naming
AsyncFn*
via the "parenthesized sugar" syntax that normalFn*
traits can be named. TheAsyncFn*
trait can be used anywhere aFn*
trait bound is allowed, such as:Other than using them in trait bounds, the definitions of these traits are not directly observable, but certain aspects of their behavior can be indirectly observed such as the fact that:
AsyncFn::async_call
andAsyncFnMut::async_call_mut
return a future which is lending, and therefore borrows the&self
lifetime of the callee.AsyncFnOnce::async_call_once
returns a future that takes ownership of the callee.dyn Fn*
trait objects) automatically implementAsyncFn*() -> T
if they implementFn*() -> Fut
for some output typeFut
, andFut
implementsFuture<Output = T>
.AsyncFn*()
trait bounds have maximum compatibility with existing callable types which return futures, such as async function items and closures which return boxed futures.impl Fn() -> impl Future<Output = ()>
does not implementAsyncFn()
, due to the fact that aAsyncFn
-if-Fn
blanket impl does not exist in reality. This may be relaxed in the future. Users can work around this by wrapping their type in an async closure and calling it. I expect this to not matter much in practice, as users are encouraged to writeAsyncFn
bounds directly.The by-move future
When async closures are called with
AsyncFn
/AsyncFnMut
, they return a coroutine that borrows from the closure. However, when they are called viaAsyncFnOnce
, we consume that closure, and cannot return a coroutine that borrows from data that is now dropped.To work around around this limitation, we synthesize a separate future type for calling the async closure via
AsyncFnOnce
.This future executes identically to the by-ref future returned from calling the async closure, except for the fact that it has a different set of captures, since we must move the captures from the parent async into the child future.
Interactions between async closures and the
Fn*
family of traitsAsync closures always implement
FnOnce
, since they always can be called once. They may also implementFn
orFnMut
if their body is compatible with the calling mode (i.e. if they do not mutate their captures, or they do not capture their captures, respectively) and if the future returned by the async closure is not lending.See the dev guide for a detailed explanation for the situations where this may not be possible due to the lending nature of async closures.
Other notable features of async closures shared with synchronous closures
Copy
and/orClone
if their captures areCopy
/Clone
.AsyncFn
orFn
trait bound, we can eagerly infer the argument types of the closure. More details are provided in the dev guide.Lints
This PR also stabilizes the
CLOSURE_RETURNING_ASYNC_BLOCK
lint as anallow
lint. This lints on "old-style" async closures:We should encourage users to use
async || {}
where possible. This lint remainsallow
and may be refined in the future because it has a few false positives (namely, see: "Where do we expect rewriting|| async {}
intoasync || {}
to fail?")An alternative that could be made at the time of stabilization is to put this lint behind another gate, so we can decide to stabilize it later.
What isn't stabilized (aka, potential future work)
async Fn*()
bound syntaxWe decided to stabilize async closures without the
async Fn*()
bound modifier syntax. The general direction of this syntax and how it fits is still being considered by T-lang (e.g. in RFC 3710).Naming the futures returned by async closures
This stabilization PR does not provide a way of naming the futures returned by calling
AsyncFn*
.Exposing a stable way to refer to these futures is important for building async-closure-aware combinators, and will be an important future step.
Return type notation-style bounds for async closures
The RFC described an RTN-like syntax for putting bounds on the future returned by an async closure:
This stabilization PR does not stabilize that syntax yet, which remains unimplemented (though will be soon).
dyn AsyncFn*()
AsyncFn*
are not dyn-compatible yet. This will likely be implemented in the future along with the dyn-compatibility of async fn in trait, since the same issue (dealing with the future returned by a call) applies there.Tests
Tests exist for this feature in
tests/ui/async-await/async-closures
.A selected set of tests:
tests/ui/async-await/async-closures/mutate.rs
tests/ui/async-await/async-closures/captures.rs
tests/ui/async-await/async-closures/precise-captures.rs
tests/ui/async-await/async-closures/no-borrow-from-env.rs
tests/ui/async-await/async-closures/higher-ranked.rs
tests/ui/async-await/async-closures/higher-ranked-return.rs
Fn*
traitstests/ui/async-await/async-closures/is-fn.rs
tests/ui/async-await/async-closures/implements-fnmut.rs
tests/ui/async-await/async-closures/clone-closure.rs
AsyncFnOnce
is calledtests/ui/async-await/async-closures/drop.rs
tests/ui/async-await/async-closures/move-is-async-fn.rs
tests/ui/async-await/async-closures/force-move-due-to-inferred-kind.rs
tests/ui/async-await/async-closures/force-move-due-to-actually-fnonce.rs
tests/ui/async-await/async-closures/signature-deduction.rs
tests/ui/async-await/async-closures/sig-from-bare-fn.rs
tests/ui/async-await/async-closures/signature-inference-from-two-part-bound.rs
Remaining bugs and open issues
AsyncFn*
family of traits in favor ofLendingFn*
#120694 tracks moving onto more generalLendingFn*
traits. No action needed, since it's not observable.polymorphization does not support coroutines from async closures
#124020 - Polymorphization ICE. Polymorphization needs to be heavily reworked. No action needed.async Fn*
trait bounds #127227 - Tracking reworking the way that rustdoc re-sugars bounds.AsyncFn
is fixed by Clean middle generics using paren sugar if trait has#[rustc_paren_sugar]
#132697.Where do we expect rewriting
|| async {}
intoasync || {}
to fail?|t: T| async {}
, which doesn't capturet
unless it is used in the async block. This may affect theSend
-ness of the future or affect its outlives.History
Important feature history
async || ...
closures into#![feature(async_closure)]
#62292AsyncFn
implementations, make async closures conditionally implFn*
traits #120712coroutine_captures_by_ref_ty
more sophisticated #123660FnMut
/Fn
if it has no self-borrows #125259Fn
+Future
bounds #127482Acknowledgements
Thanks to @oli-obk for reviewing the bulk of the work for this feature. Thanks to @nikomatsakis for his design blog posts which generated interest for this feature, @traviscross for feedback and additions to this stabilization report. All errors are my own.
r? @ghost