If you have any experience with async programming in JavaScript (or in Go, C#, etc), you probably do something like this on a regular basis:
async function makeRequest() { ... }
let task = makeRequest();
// do some other stuff while request is in-flight
let result = await task;
Parallelism is the whole point of asynchronous programming. It allows us to start a task in the background and then do something else while waiting for it to complete. Naturally you should assume the same code works in Rust:
async fn make_request() { ... }
let task = make_request();
// do some other stuff while request is in-flight
let result = task.await;
In fact, this code doesn’t work. Unlike every other programming language I am
aware of (except maybe Haskell), calling make_request
does nothing. The
request isn’t even started until we get to task.await
. The correct version of
this is:
async fn make_request() -> Request { ... }
let request = make_request(); // construct a future but do nothing to run it
let task = Task::spawn(request); // actually send request via the global executor
// do some other stuff while request is in-flight, for real this time
let result = task.await; // poll the executor's completion of the task, not the task itself
This is because Rust’s async
is lazy. And async fn
is simply a sugar for:
fn make_request() -> impl Future<Output=Request> {
async {
...
}
}
This desugar makes the behavior of make_request
much more clear: it returns an
async block without making any effort to execute the async block. New users
might still be surprised that
let _ = async { println!("Hello!") };
never prints hello. But other languages don’t have a close equivalent to async { }
so any confusion is less likely to persist.
So should we prefer fn -> impl Future
over async fn
? Is async fn
considered harmful? Probably not. The async fn
syntax does a lot to eliminate
verbosity. It allows users to ignore all sorts of impl Future + Sync + Send + '_
shenanigans, which new users will certainly find valuable. We don’t even
want to change the desugar to something like
fn make_request() -> impl Future<Output=Request> {
Task::spawn(async {
})
}
because it breaks Rust’s ability to inline and optimize the common
make_request().await
case, which users rely on to produce all kinds of
zero-cost async abstractions.