Transition #[async]
functions
- from
-
being written with a
Result
return type that gets rewritten inside the macro toimpl Future
, e.g.#[async] fn fetch_rust_lang(client: hyper::Client) -> io::Result<String>
- to
-
being written with an
impl Future
or similar return type directly, e.g.#[async] fn fetch_rust_lang(client: hyper::Client) -> impl Future<Item=String, Error=io::Error>
Also support return types of impl Stream
, Box<Future>
, impl StableFuture
etc. (potentially any CoerceUnsized
smart pointer if possible?).
When browsing the documentation, then moving to the source code it is confusing to see different signatures. By specifying the expected signature directly this will be avoided.
If a library is planning on using #[async]
in the future then they can prepare
today by declaring all their functions -> impl Future
and returning explicitly
implemented futures. They will then simply have to add the #[async]
attribute
and rewrite the body, whereas today they would be forced to change the signature
as well:
+#[async]
+fn fetch_rust_lang(client: hyper::Client) -> io::Result<String> {
-fn fetch_rust_lang(client: hyper::Client) -> impl Future<Item=String, Error=io::Error> {
pub abstract type CountDown: Future<Item = (), Error = !>;
#[async]
pub fn count_down(count: Duration) -> CountDown {
...
}
pub trait CountDown {
fn start(&mut self, count: Duration) -> impl Future<Item = (), Error = !> + '_;
}
impl CountDown for Timer {
#[async]
fn start(&mut self, count: Duration) -> impl Future<Item = (), Error = !> + '_ {
...
}
}
It's possible to support all return types via just a single macro. This may still require arguments for boxing or pinning the return type, but it might be possible to automatically infer this from the return type as well.
All existing statically typed async
/await
supporting languages use this form
of signature. See Prior Art below for examples.
Update all examples:
s/-> Result<(.+), (.+)>/-> impl Future<Item = \1, Error = \1>/
Warning, highly cribbed from the C# async programming docs, not suitable for direct inclusion in Rust docs.
The #[async]
and await!
macros are the heart of asynchronous programming in
Rust. By using these two keywords you can easily and succinctly create
asynchronous methods without the overhead of define creating structs and writing
manual Future
implementations.
The following example shows an asynchronous method. Almost everything in the code should look completely familiar to you. The comments call out the features that you add to create the asynchrony.
// Two things to note in this signature:
// - The function has the `#[async]` macro applied to it.
// - The return type is `impl Future`
// See below for the description of the `unpinned` argument.
#[async(unpinned)]
fn fetch_rust_lang(client: hyper::Client) -> impl Future<Item=String, Error=io::Error> {
// hyper::Client::get returns a `Future<Item=Response>`. That means that
// when you await the future you'll get a `Response`
let responseTask = client.get("https://www.rust-lang.org");
// You can do work here that doesn't rely on the response.
//
// Wether or not this will actually run in parallel depends on whether
// `hyper::Client::get` spawned its work off onto a thread pool internally.
do_something_else();
// The `await!` macro suspends `fetch_rust_lang`
// - `fetch_rust_lang` can't continue until `responseTask` is complete
// - Meanwhile, the parent future that called `fetch_rust_lang` can perform
// other work of its own
// - Control will resume here once `responseTask` is complete
// - The `await!` macro then returns a `Result<Response, io::Error>`
// containing the result or error returned from `responseTask` (this is
// handled via the `?` operator here).
let response = await!(responseTask)?;
// If something goes wrong, you can return a `Result::Err` value to
// shortcircuit this Future.
if !response.status().is_success() {
return Err(io::Error::new(io::ErrorKind::Other, "request failed"))
}
// `response.body().concat()` returns another `Future<Item=Vec<u8>>` which
// will resolve with the complete body of the response once it's downloaded.
let body = await!(response.body().concat())?;
let string = String::from_utf8(body)?;
// The return value must be a `Result` of the same `Item` and `Error` types
// as the signature declares. Any methods that are awaiting
// `fetch_rust_lang` will be able to resume with the returned value.
Ok(string)
}
The #[async]
macro defaults to creating potentially self-referential impl StableFuture
, to instead create movable non-self-referential impl Future
you
must pass the unpinned
argument. Similarly if you wish to create a boxed
future you can use #[async(boxed)] fn foo() -> Box<Future>
.
#[async]
will not touch the provided signature in any way. Most of the
generated code will be similar to what it is today. The futures-async-runtime
will only contain 2 different wrapper types, one to support
StableFuture
/StableStream
and one to support Future
/Stream
. The wrappers
will implement Future
or Stream
(or the Stable
variants of) based on the
yield and return type of the generated closure.
If it's possible to detect a Box
return type automatically then this will be
done and any Box<...>
return type will be boxed. Otherwise, the user will
still have to pass a boxed
argument as today when they specify Box<...>
as a
return type.
While this changes the written return type, it doesn't affect what the user is
allowed to return from the generator. This is still forced to be a Result
of
some kind (although, this could likely be extended to Try
). This inconsistency
between the functions declared return type and the function body's actual return
is what prompted the original design of the macro.
Because of how impl Trait and trait objects handle lifetimes, this interacts very poorly with lifetime elisions. Specifically, the return type only captures explicitly called out lifetimes, but the returned generator necessarily captures all lifetimes.
So say you have a method like this:
#[async] fn foo(&self, arg: &str) -> Result<i32, io::Error>You'd have to write it like this:
#[async] fn foo<'a, 'b>(&'a self, arg: &'b str) -> impl Future<Item = i32, Error = io::Error> + 'a + 'bThe alternative would be to make lifetime elisions not work the same way the work with regular impl Trait return types, but that seems clearly out to me, since the whole idea of this change is to make the return type accurately reflect with the function returns.
I argue this is not a true downside. This is just the current state of impl Trait
, if this is a major issue for #[async]
then it will be a major issue
for other users of impl Trait
and should be fixed at the source.
It also seems to be closer to the current thinking around lifetimes in signatures, along with the plan to support explicit elided lifetimes this is ensuring that reading the function signature will allow you to quickly determine the lifetime dependencies.
- Why is this design the best in the space of possible designs?
- What other designs have been considered and what is the rationale for not choosing them?
- What is the impact of not doing this?
C#:
async Task<int> AccessTheWebAsync()
{
HttpClient client = new HttpClient();
string urlContents = await client.GetStringAsync("http://msdn.microsoft.com");
return urlContents.Length;
}
async function foo(): Promise<number> {
await delay(100);
return 5;
}
Dart:
If the expression being returned has type
T
, the function should have return typeFuture<T>
(or a supertype thereof). Otherwise, a static warning is issued.
Hack:
async function curl_A(): Awaitable<string> {
$x = await \HH\Asio\curl_exec("http://example.com/");
return $x;
}
-
Is it possible to automatically detect
Box
return types?- This would allow removing the
boxed
argument to the#[async]
macro. - Extending from this, could this then allow any
CoerceUnsized
smart pointer to be used?
- This would allow removing the
-
Could generators be changed to not require a different syntax to support self-references?
- For example if they just automatically don't implement
Unpin
if they have a self-reference then this could allow removing theunpinned
argument to the#[async]
macro and just rely on whetherUnpin
is implemented.
- For example if they just automatically don't implement
-
If it's not possible to remove the
unpinned
argument, shouldunpinned
be the default, and what should the exact argument be (pinned
,move
,unpin
,stable
, ...)?