Would implicit `await` really be a bad idea?

To be clear, that already needs to be done: most things are based on Futures 0.1, but they will need to move to Futures 0.3 (which is a breaking change).

In addition, we're not talking about changing anything about Future, just the behavior of async (which is not stabilized, and isn't as widely used as Future is). So existing stuff does not need to be thrown away.

1 Like

Something like this should work (I did some light testing):

use std::pin::Pin;
use std::task::{Poll, LocalWaker};
use futures_util::future::FutureExt;

pub struct Force<A> {
    future: Option<Pin<Box<A>>>,
}

impl<A> Force<A> {
    #[inline]
    pub fn new(future: A) -> Self {
        Self {
            future: Some(Box::pin(future)),
        }
    }
}

impl<A> Unpin for Force<A> {}

impl<A> Future for Force<A> where A: Future {
    type Output = Deferred<A>;

    fn poll(mut self: Pin<&mut Self>, lw: &LocalWaker) -> Poll<Self::Output> {
        let mut future = self.future.take().unwrap();

        match future.poll_unpin(lw) {
            Poll::Ready(value) => Poll::Ready(Deferred {
                state: DeferredState::Ready(Some(value)),
            }),

            Poll::Pending => Poll::Ready(Deferred {
                state: DeferredState::Pending(future),
            }),
        }
    }
}


enum DeferredState<A> where A: Future {
    Ready(Option<A::Output>),
    Pending(Pin<Box<A>>),
}

pub struct Deferred<A> where A: Future {
    state: DeferredState<A>,
}

impl<A> Unpin for Deferred<A> where A: Future {}

impl<A> Future for Deferred<A> where A: Future {
    type Output = A::Output;

    fn poll(mut self: Pin<&mut Self>, lw: &LocalWaker) -> Poll<Self::Output> {
        match &mut self.state {
            DeferredState::Ready(value) => Poll::Ready(value.take().unwrap()),
            DeferredState::Pending(future) => future.poll_unpin(lw),
        }
    }
}

You use it like this:

async fn foo() {
    let x = await!(Force::new(bar()));

    ...

    let y = await!(x);
}

Basically, Force::new returns a Future which immediately polls bar(), and then you can later retrieve the value by awaiting on x.

This does have some overhead (a Box heap allocation), but perhaps that can be fixed with some trickery.

I’m not actually sure how to express this with implicit await. Maybe something like this?

async fn foo() {
    let x = async { Force::new(bar()) };

    ...

    let y = await!(x);
}

I haven’t looked too deeply at the various implicit async proposals, so I’m not sure how they handle deferred Futures.

I know they usually use async { ... } to do the actual deferral, but then how do they await the deferred Future?

That’s the reason I used async || { .. }. The idea in “implicit await” is that the explicit async creates a deferred execution, which is equivalent to a closure in sync code.

Personally, in a implicit await I think I’d write that as

async fn foo() {
    let x = Async::start(bar);
    ..
    let y = x.await();
}

I do think the system requires a type (trait) for “synchronous asynchronous execution” which is async fn and a separate type (trait) for “deferred asynchronous execution” which is async || { .. }.

1 Like

I like this. It feels very clean, and it’s shorter than the explicit await.

I also like the strict separation between auto/non-auto Futures, with Async::start and .await() to convert between them.

As I understand it, Async::start and .await() are not magic at all, they are ordinary structs/methods.

Defining the .await() method seems fairly trivial, but how would the Async::start function be defined? It would have to take in the return value of an async fn, but without the auto-await.

I guess it could be used like this instead:

let x = Force::new(async || bar());

That seems quite reasonable as well.

Actually, I don’t think it’s necessary to use the async || { ... } syntax.

The reason is because await does not work inside of closures (unless the closure is async, of course). In other words, async/await is shallow, just like JavaScript async/await.

So in order to prevent the auto-await, an ordinary function works just fine:

async fn foo() {
    // bar() is not awaited
    let x = || bar();
    ...
    // x() is awaited
    let y = x();
}

So that means we don’t need two traits! We can use impl Future for both the auto and non-auto Futures! And async fns can return impl Future!

Any delaying of Futures is simply done with closures, which is already the standard way to delay synchronous code!

I was pretty ambivalent about implicit await before, but this is really making me like it.

And of course the Deferred struct can be changed to have a completely non-magical await method which simply returns impl Future:

enum DeferredState<A> where A: Future {
    Ready(Option<A::Output>),
    Pending(Pin<Box<A>>),
}

impl<A> Unpin for DeferredState<A> where A: Future {}

impl<A> Future for DeferredState<A> where A: Future {
    type Output = A::Output;

    fn poll(mut self: Pin<&mut Self>, lw: &LocalWaker) -> Poll<Self::Output> {
        match *self {
            DeferredState::Ready(value) => Poll::Ready(value.take().unwrap()),
            DeferredState::Pending(future) => future.poll_unpin(lw),
        }
    }
}

pub struct Deferred<A> where A: Future {
    state: DeferredState<A>,
}

impl<A> Deferred<A> where A: Future {
    #[inline]
    pub fn await(self) -> impl Future<Output = A::Output> {
        self.state
    }
}

And now it can be used like this:

async fn foo() {
    let x = Force::new(|| bar());
    ...
    let y = x.await();
}

This is really great! Quite a bit nicer than explicit await.

2 Likes

I haven't read this whole thread, but I wanted to point out some interesting research on this topic. Jaeheon Yi and Tim Disney, working with Freund and Flanagan, studied an extension to Java with "cooperative concurrency" -- where you explicitly identify synchronization points with yield. The idea of their system was that the compiler put in enough locks that this was the only point you could observe "nonatomic execution".

If memory serves, one of the things they looked at was how quickly people fixed threading bugs. They found that, indeed, people fix bugs faster if they have explicit yield.

Just a datapoint.

(I don't think it's the only consideration, mind you, but it is an argument in favor of explicit await.)

13 Likes

Hi, I just find this discussion about implicit await. I’m reposting my comment on Github. Just want to know if it’s a good idea to be explicit about implicit await since there is no feedback so far there.

We can first have prefix await landing:

await { future }?

And later we add something similar to

let result = implicit await { client.get("https://my_api").send()?.json()?; }

When choosing implicit mode, everything between {} are automatically awaited.

This has unified await syntax and would balance the need for prefix await, chaining, and being as explicit as possible.

They found that, indeed, people fix bugs faster if they have explicit yield.

@nikomatsakis There are definitely benefits to explicit await. The only question is if the benefits are worth the costs. The supporters of implicit await are also talking about "partial implicit await" which tries to balance the signal to noise of each additional await in the codebase.

A proposal I've made, or another similar idea from the github thread.

Would love to hear your thoughts on such a compromise.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.