A nightly Rust compiler is required until the async_await feature is stabilized.
Add this to your Cargo.toml
:
[dependencies]
implicit-await = "0.1"
Now you can use #[implicit_await]
:
use implicit_await::implicit_await;
#[implicit_await]
async fn foo() {
// Any functions returning a future (`async fn` or `fn -> impl Future`) will be automatically awaited.
}
Rust's async/await feature uses the postfix .await
operator to explicitly request suspension points within async
functions. The discussion around the syntax of the .await
operator has been long and contentious, driven by the desire to interoperate smoothly with Rust's existing postfix ?
operator (among other concerns).
When I was reading about the various syntax proposals from which .await
finally emerged the leader, it occurred to me that the problems would be greatly simplified by making the wrapping/unwrapping behavior of async
functions internally consistent. In standard Rust, async
functions implicitly wrap their return values in a Future
, but the inputs to async
functions - the child Futures
that determine the shape of the compiler-generated state machine - are explicitly unwrapped via .await
. If instead async
functions handled both wrapping and unwrapping implicitly, then there would be no need for the .await
operator, bypassing the syntax concerns about the operator.
This library provides a procedural macro #[implicit_await]
which transforms async
functions to implicitly unwrap child Future
s.
Standard Rust | #[implicit_await] | |
---|---|---|
Outputs (function results) are... | implicitly wrapped | implicitly wrapped |
Inputs (child futures) are... | explicitly unwrapped (.await) | implicitly unwrapped |
See the sum example for a full working demo of the following.
Suppose you have the following three functions, which range from fully synchronous to fully asynchronous.
type IntResult = Result<u32, ()>;
// Fully synchronous
fn num_sync(num: u32) -> IntResult {
Ok(num)
}
// Half-and-half
fn num_fut(num: u32) -> impl Future<Output = IntResult> {
ready(Ok(num))
}
// Fully asynchronous
async fn num_async(num: u32) -> IntResult {
Ok(num)
}
In standard Rust, calls to synchronous and asynchronous functions are distinguished by the need to postfix the .await
operator.
async fn sum() -> IntResult {
// No .await - a synchronous function.
let one: u32 = num_sync(1)?;
// .await - asynchronous function.
let two: u32 = num_fut(2).await?;
// .await - asynchronous function.
let three: u32 = num_async(3).await?;
// Note that the return value is implicitly wrapped.
Ok(one + two + three)
}
With implicit-await
, synchronous and asynchronous functions behave equivalently due to the implicit awaiting behavior. This allows the ?
operator to work as expected without any additional syntax.
#[implicit_await]
async fn sum() -> IntResult {
// Synchronous function.
let one: u32 = num_sync(1)?;
// A `fn -> impl Future` is implicitly awaited.
let two: u32 = num_fut(2)?;
// An `async fn` is implicitly awaited.
let three: u32 = num_async(3)?;
Ok(one + two + three)
}
implicit-await
makes calls to asynchronous functions behave identically to calls to synchronous functions from the point of view of program control flow. The intuitive rule that functions complete before they return, which Rust programmers naturally learn from calling synchronous functions, also applies to asynchronous function calls. However, sometimes you have use-cases where you want explicit control over suspension points. The defer!
macro allows you to defer awaits so that you can start multiple asynchronous processes in parallel. A common example of this use case is making multiple network requests at the same time, then waiting for all of them to complete.
use implicit_await::defer;
#[implicit_await]
async fn sum() -> IntResult {
// Start three futures in parallel without implicitly awaiting.
let (one, two, three) = defer!{ (
num_fut(1),
num_async(2),
num_async(3)
) };
// This `join` call is implicitly awaited because the defer! block ended.
let (one, two) = futures::future::join(one, two);
// If you have a bare `Future`, you're free to .await it if you want.
let three = three.await?;
Ok(one? + two? + three);
}
In a nutshell: error[E0599]: no method named `as_future` found for type `your::type::here` in the current scope
The catch is that Rust doesn't support negative or mutually exclusive trait bounds. Because of this, I needed to make a choice whether to support Future
types seamlessly and !Future
types painfully, or !Future
types seamlessly and Future
types painfully. I chose the former. This means that types which do not implement Future
need to be manually supported.
You're calling sync functions that return... | Then you need to... |
---|---|
Types from std |
Do nothing ("std" is a default feature) |
Types from a third-party crate | Check the features list to see whether implicit-await already supports that crate, and if so enable the feature. Otherwise submit an issue requesting support for that crate's types be added. Rockstars can include a PR to add support along with the issue! |
Types from your own crate | Use the as_future! macro to add support in your own code. OR submit a PR to implicit-await to support your crate behind a feature flag. |
std
(default-feature) - Enable support for Rust's standard library types. Disable this feature forno-std
support.
- Fix bug in the as_future! macro which makes multi-generic invocations not work.
- Go through all of
std
adding types - Come up with a list of other crates to add support for, and do so.
I strive to limit dependencies where possible. The direct dependencies of the implicit-await
project are proc-macro2
, quote
, and syn
, all used to implement the #[implicit_await]
procedural macro. unicode-xid
is brought in as a transitive dependency.
implicit-await v0.1.0
└── implicit-await-macro v0.1.0
├── proc-macro2 v0.4.30
│ └── unicode-xid v0.1.0
├── quote v0.6.12
│ └── proc-macro2 v0.4.30 (*)
└── syn v0.15.34
├── proc-macro2 v0.4.30 (*)
├── quote v0.6.12 (*)
└── unicode-xid v0.1.0 (*)
Apparently futures in Kotlin work similarly to this.