futures-await icon indicating copy to clipboard operation
futures-await copied to clipboard

Inferred `await!`

Open yazaddaruvala opened this issue 7 years ago • 32 comments

What are your thoughts on a version of this syntax where await! is inferred?

Example:

#[async]
fn fetch_rust_lang(client: hyper::Client) -> io::Result<String> {
    let response = await!(client.get("https://www.rust-lang.org"))?;
    if !response.status().is_success() {
        return Err(io::Error::new(io::ErrorKind::Other, "request failed"))
    }
    let body = await!(response.body().concat())?;
    let string = String::from_utf8(body)?;
    Ok(string)
}
#[async_await]
fn fetch_rust_lang(client: hyper::Client) -> io::Result<String> {
    let response = client.get("https://www.rust-lang.org")?;
    if !response.status().is_success() {
        return Err(io::Error::new(io::ErrorKind::Other, "request failed"))
    }
    let body = response.body().concat()?;
    let string = String::from_utf8(body)?;
    Ok(string)
}

yazaddaruvala avatar Jun 28 '17 02:06 yazaddaruvala

I personally prefer an explicit keyword or macro over "magic". In the first example you can skim it quickly and see that it has two places where it may wait on blocking I/O. While the in the second example I first have to check all the APIs of the hyper client to check if a method returns a future. For example I might wrongly assume that the complete body is ready once client.get returns.

Thomasdezeeuw avatar Jun 28 '17 11:06 Thomasdezeeuw

Perhaps! I don't know how this would be implemented, but would be worth a shot!

alexcrichton avatar Jun 30 '17 05:06 alexcrichton

I'm curious about what the benefit would be here.

jtremback avatar Aug 07 '17 16:08 jtremback

In C#, sometimes you would not use await, if it is necessary to juggle the returned tasks (put them in a list, await several at once, await elsewhere, etc). I imagine the same use cases are applicable here too.

Inferring await means awaiting at the first sight of any future in a method marked as #[async_await], which takes away some control. However, #[async_await] still sounds interesting, and looks like it could live alongside #[async].

Nercury avatar Aug 08 '17 07:08 Nercury

#[async_await] still sounds interesting, and looks like it could live alongside #[async]

@Nercury your example is exactly why await! still needs to exist. Starting a collection of futures, then awaiting each is a very common usecase (I'd love if we could find a good way to infer it too, but that may get into the realm of magic). This is another situation where you wouldn't want to infer await: await!(X.or(Y)). I named them differently on purpose: #[async_await] and #[async].

@jtremback The reason I prefer inferring await! as much as possible is: In a service of any kind, every method is likely going to be #[async] and littered with await!. In this context, it just becomes a redundant "keyword" and annoying to remember to use it on each function which returns impl Future.

yazaddaruvala avatar Aug 08 '17 16:08 yazaddaruvala

All Futures are fallible in that there is an associated Error type. Meaning in general usage you'll never have a await!( ... ) without being trailed with a ?.

let response = await!(make_request())?;

I saw this briefly discussed in the eRFC. I think if we have ? act like . (meaning auto dereferencing) and implicitly "break into" the Future, it will make a lot of logical and ergonomic sense.

let response = make_request()?;

If you wanted to "build up" a bunch of futures and wait for them concurrently, you'd do so normally without suffixing them with ?.

let requests = vec![make_request(), make_request(), make_request()];
let responses = future::join_all(requests)?; // implicit await here

As for implementation, I don't see how until the compiler knows about generators and the await! transformation can be a compiler pass.

mehcode avatar Aug 15 '17 09:08 mehcode

@mehcode wrote:

All Futures are fallible in that there is an associated Error type. Meaning in general usage you'll never have a await!( ... ) without being trailed with a ?.

Maybe my comment on Reddit might be useful here:-

await!(...)? feels unnecessary and redundant to me. If we do it that way, I don't even see the need for a separate #[async_await] macro. The await macro itself still makes sense to keep when one doesn't want to propagate the error, eg:-

match await!(...) {
    Ok(result) => { /* do something with the result */ }
    Err(error) => { /* handle the error */ }
}

Both styles can coexist within the same #[async] block. Of course one could still do await!(...)? if they prefer but I hope we will have a lint for that.

rushmorem avatar Aug 30 '17 16:08 rushmorem

@yazaddaruvala I'm not very experienced in Rust, and I will admit that I have not even used this library!

But I was working on a piece of javascript where I had forgotten to await something which caused all kinds of problems and I thought about this issue. It seems to me that the vast majority of times that you're using an async function, you await it immediately when calling it. The only reason not to await it is if you want to do something with the underlying future/promise, like mapping over it.

So it seems to me that perhaps another syntax choice would be to await automatically, unless a get_future!() macro is used or something like that.

Otherwise, I could see myself using #[async_await] on every async function, unless there was a specific future in the function that I did not want to await, at which point I would have to scroll back up to the top of the function and change #[async_await] to #[async], then go back and add await!() everywhere.

Seems cleaner to make that annotation right where it's important, instead of at the top of the containing function.

It also might make it easier for people new to cooperative multitasking, or programming in general to learn Rust. You would not have to worry about futures, until you needed to drop down in a level of abstraction to do something more advanced. With conventional await, the abstraction is still right in your face.

jtremback avatar Aug 31 '17 15:08 jtremback

@jtremback I respectfully disagree with your opinion that an automatic await would make the code easier. I think it would make it a lot harder to read, specifically you're no longer sure what a function does or returns. If I see an explicit await keyword, or await! macro, I instantly know that function call could be waited on, rather then it returning something immediately. See the example below for an idea of what I mean.

#[async]
fn some_function() -> Result<(), ()> {
    let value1 = await some_other_function()?; // An await function.
    let value2 = do_something(value1)?; // Regular function.
    let value3 = await a_third_function(value2)?; // An await function.
    Ok(value3);
}


#[async]
/// Example without explicit await.
fn some_function2() -> Result<(), ()> {
    let value1 = some_other_function()?; // Maybe a regular function, maybe an await function...
    let value2 = do_something(value1)?; // Maybe a regular function, maybe an await function...
    let value3 = a_third_function(value2)?; // Maybe a regular function, maybe an await function...
    Ok(value3);
}

A somewhat similar debate is going on in RFC 2111, where the idea is to automatically deference Copy traits. In that debate that people argue that with the current requirement of adding an explicit reference the code is a lot easier to read. And I have to agree.

Furthermore asynchronous or concurrent code is hard. Making it seem easy, by abstracting things away that people just need to know about when programming something that works in parallell, will come to bite them later.

Thomasdezeeuw avatar Aug 31 '17 17:08 Thomasdezeeuw

If awaiting is going to be the default, it needs to be the default everywhere, not piggybacked on ?. (Getting a future rather than awaiting would then need a syntactic marker instead.)

One potential further benefit of this swapped default would be generic code. A function could be instantiated as either blocking or async, without changing its body. This would mostly be useful for utility/combinator-style functions, which often take closures (which would instead be generators in the async case).

Confusing await with ? makes that sort of thing more complicated and messy.

rpjohnst avatar Aug 31 '17 18:08 rpjohnst

Both synchronous reads and asynchronous reads with await! will logically block your function. I'm not seeing the disconnect in logical behavior so the additional keyword is pure noise to me.

#[async]
fn foo() -> Result<_, _> {
  let value1 = sync_read()?;
  let value2 = await!(async_read())?;
}

Let's take a moment and read that function again. There is a significantly more dangerous problem going on that is currently impossible to prevent or lint against.

Synchronous I/O in an Asynchronous function blocks the entire event loop. In Node.js we are able to lint against this because of a convention that all sync methods end in sync. That's still pretty flimsy in practice.

Yes, this includes even your warn! log macro, though we don't need to await! that.

This should probably be its own issue though.

mehcode avatar Aug 31 '17 19:08 mehcode

@Thomasdezeeuw I was also skeptical earlier in the thread. I was just observing that in my experience of a year or more of using async/await in JS, it is far, far more common to await an async function call immediately than to do stuff with the underlying promise. Anyway, I'm only suggesting the get_future syntax be used inside OP's proposed #[async_await], not everywhere.

// Bad pseudocode
#[async_await]
fn some_function2() -> Result<(), ()> {
    let value1 = some_other_function()?;
    let future2 = get_future!(do_something(value1));
    Ok(future2);
}

Also, I'm wondering what kinds of errors you feel the explicit await prevents. When I started thinking about it, I couldn't think of that many.

jtremback avatar Aug 31 '17 21:08 jtremback

@jtremback It's mainly being explicit about what you're doing. Calling an await function is not the same as calling a regular function. If the function would block it will return something like future's Async::NotReady (or a GeneratorState) and then the function has to be called again. This is an important distinction to know and in using this the programmer has choices to make. One of the great things about Rust that it has little surprises, e.g. if it allocates it's explicit about it, this would be a step into the wrong direction in my opinion. For an example, consider the following:

// Obviously an `AtomicUsize` would be better, but for the sake of an example...
let some_counter = Mutex<usize>;

fn increase_counter() -> Result<usize, ()> {
    // We're locking the mutex here.
    let counter = await some_counter.lock();

    // What happens here if `some_other_function` would block?
    // 1. Do we unlock the mutex, and try again?
    // 2. Do we keep the lock and wait until `some_other_function` is ready?
    let some_other_value = await some_other_function();

    // Without the `await` keyword/macro it's not clear that the function might
    // block and it could be overlooked. Which would cause the lock to be held
    // for a long time, but it's not obvious. Which could lead to a performance problem.

    *counter += 1;
    Ok(*counter)
}

Javascript hides a lot of the performance costs, but Rust isn't Javascript. I know the code might "look better", or be easier to write, without the keyword/macro call, but it won't be clear enough. I see await as a cost and that shouldn't be hidden away, that (in my opinion) it not Rust's way of doing things.

Thomasdezeeuw avatar Sep 01 '17 13:09 Thomasdezeeuw

Calling an await function is not the same as calling a regular function.

You are assuming that all regular functions that will be called in an async macro will be super fast and that they will return immediately. While this would be true in an ideal world, in practice, this is not always the case. Some sync functions might be expensive but you can't tell just by looking at them.

I totally understand your argument and I agree that those await! macros do make it obvious at a glance that the function or method we are calling actually returns a future and that the await! is blocking a co-routine but what about those expensive sync functions that not only block your co-routines but actually block the thread itself yet you can't see that by merely looking at the code?

My point is, there are trade-offs with both approaches. There are even practical advantages and disadvantages with both approaches. You (and others who argue for not "inferring" await!) have correctly stated that it potentially hides performance costs. As for me, I'm all for implementing @mehcode's suggestion because, IMHO, it improves ergonomics of async code.

Let's not forget that the whole reason of introducing async/await is to improve the ergonomics of writing async code. We like it because it helps us write async code that look just like sync code. So let's not be quick to dismiss yet another advancement towards that direction.

rushmorem avatar Sep 01 '17 20:09 rushmorem

@rushmorem I agree that async/await is great for improving ergonomics, but that is not a reason to hide the performance costs. A lot of higher level languages hide heap allocations, however Rust requires you to explicitly to use a Box. Sure requiring struct myStruct { t: Box<SomeType> } is less ergonomic then struct myStruct { t: SomeType }, but you know what you're getting into. Is adding a single await keyword that much worse?

Thomasdezeeuw avatar Sep 02 '17 11:09 Thomasdezeeuw

I don't understand the suggestion that inferring await when using the ? operator "hides performance costs." The whole point of async/await is to make async code look like sync code. When you do let value = some_function()?; it makes no difference whether it's async or not.

jimmycuadra avatar Sep 03 '17 10:09 jimmycuadra

When you do let value = some_function()?; it makes no difference whether it's async or not.

I disagree, and not because of performance costs.

In my opinion, the point of async/await is to make async code more convenient while keeping explicit the points where control can be yielded to another task.

If you don't care about yield points being explicit, then why not settle for stackful coroutines? Or for plain old threads? (I'm not convinced that "performance" is a persuasive answer.)

As I see it, the main reason to use asynchronous code is so that you can have concurrent tasks that share data without needing to synchronize. If you want to make a nontrivial mutation to that shared data (e.g. say the shared data contains two tables that need to be keep consistent with each other), then you need to make sure that no other task can concurrently access the data while the mutation is being made. If you are using stackful coroutines or threads, this probably means grabbing a mutex. If you are using async/await, you know statically that as along as you don't await, then you are in a critical section and have exclusive access to the data. So there's no need to grab a mutex.

dwrensha avatar Sep 03 '17 13:09 dwrensha

Wow, I wasn't expected such an opinionated debate. I've been on vacation and haven't had time to digest any of this just yet, but I've summarized the thread below. Hopefully thats helpful. I'll add some more thoughts when I've been able to formulate them.

Currently, it seems this type of inference is not possible. I'm not going to focus on this, and maybe we can see if we would even want such a feature.

Reasons Not To:

  • Comes off as "magic":
    • It is not explicit which line takes a long time.
    • Async code is hard. Making it seem easy, will come to bite the user later.
  • Less control:
    • Need to start Futures in a loop; Need to wait on Futures in a different loop.
    • Cannot await! two futures (i.e. f1.or(f2), f1.and(f2)).
    • Hiding "the points where control can be yielded" make it harder to deal with shared state.

Reasons To:

  • Service code, would be littered with await!.
    • It becomes more of a nuisance adding it to every function which returns impl Future.
  • More ergonomic / learnable.
    • As a new programmer, you just write the code you're used to.
  • Would allow for a potential future with code generic across async or sync io.

Other Options:

  • Reuse ? to infer await!
    • [Rebuttal] conflating these ideas is more complicated and messy

yazaddaruvala avatar Sep 15 '17 17:09 yazaddaruvala

This thread has some more discussion, based on Kotlin's flavor of async/await: https://www.reddit.com/r/rust/comments/6zy8hl/kotlins_coroutines_and_a_comparison_with_rusts/

Treating async like unsafe (an effect)

One idea that offsets the "magic" of inferring await! is to treat asynchronicity more like we treat unsafe:

  • Individual suspension points are not marked, but blocks of async code still are, especially if we:
  • Make it an error to call an async function outside of an async context, and allow non-closure generators.

This is less noisy and a more familiar (re unsafe) way to mark potentially-weird code. It also has the further benefit, beyond explicit await!, that you can't accidentally fire off an async operation and forget to wait on it. This is most apparent when the operation doesn't return a value, or you are ignoring its return value, since you at least get a type error if you try to use an impl Future as a synchronous value.

This also opens the door to making code generic over asynchronicity. You could, for example, pass either a closure or a generator into a generic function and have it be synchronous or asynchronous. This would be immensely useful for a case like this:

table.entry(key).or_insert_with(|| {
    // do some async work to compute the new value
    // with async-generic code, `or_insert_with` doesn't have to decide on using `await!` or not
})

Working with Futures directly

Implicit suspension points don't have to take away any control. They just shift the explicit annotation to the case where you aren't immediately suspending. This may actually be better, because those are the cases that control flow starts getting weird. It also reinforces the above point, that async functions are different and can't be called the same way.

For example, you might have a library function that accepts a generator as an argument and returns an impl Future, rather than forcing every async API to construct an impl Future itself. This has the added flexibility of letting the client control (e.g. via arguments to that function) where the generator runs, which is important for things like UI frameworks that care about what thread they're called from.

rpjohnst avatar Sep 15 '17 18:09 rpjohnst

Sorry, this is kinda tangential, but a lot of people in this thread have an issue with either calling blocking APIs in an async context, or blocking a future in an async context. I'm curious if there is a way to allow creators of APIs to inform API users of such instances. Then API creators could mark functions as blocking because of io, or because of long running CPU tasks (at their discretion like with unsafe). The compiler would then help warn users against silly mistakes / at least it would be easily grep-able in the worst case.

@alexcrichton given your proximity to the compiler and async development, what are your informal thoughts on such a language feature: A blocking tag would work exactly like an unsafe tag does today. i.e. If a function is declared blocking it can only be used in another function or block labeled blocking.

I'm not suggesting changing the std library (i.e. mark all blocking calls), that would be backwards incompatible but at least allowing crates like futures to declare its API with this "tag" would be cool.

Finally, It would be ideal if #[async] would ensure the function cannot be blocking.

eg.

fn blocking block_until() {
  ...
}

fn _____ {
  block_until(); // Compiler Error
}

fn blocking _____ {
  block_until(); // Will work.
}

fn _____ {
  blocking {
    block_until(); // Will work.
  }
}

#[disallow_tag(blocking)]
fn blocking _____ { // Compiler error
  ...
}

#[async] // Async applies disallow_tag(blocking)
fn blocking _____ { // Compiler error
  ...
}

#[async] // Async applies disallow_tag(blocking)
fn _____ {
  blocking {
    block_until(); // Will work, but it is very explicit. The user really should know better.
  }
}

P.S. It might even be worth making the blocking tag act like unsafe but be a warning instead of an error. Then add a feature which enforces blocking as an error. i.e. #![allow(enforce_function_tags(blocking))] where unsafe is always whitelisted to enforce.

P.S. I'm not tied to the word "blocking" just the idea of such a tag seems interesting.

yazaddaruvala avatar Sep 20 '17 03:09 yazaddaruvala

@yazaddaruvala I think that's effectively an effect system, which I don't think will work out well. I think unsafe as an effect system works well because everyone avoids it like the plauge, but for example integration of any effect system into traits would be a nightmare (duplicate traits, unnecessary tags, etc)

alexcrichton avatar Sep 20 '17 16:09 alexcrichton

I love that they have a name for everything! But I can see what you mean about it overwhelming many code bases, which would be a poor experience.

yazaddaruvala avatar Sep 20 '17 17:09 yazaddaruvala

@yazaddaruvala @alexcrichton I was trying to design a similar lint when I was working on an async/await RFC several years ago. I ultimately abandoned the idea when I realized that there is no clearcut distinction at all: When a function is neither async nor blocking, it's supposed to be "basically instant", taking ~0 time to execute. But chain up enough of those "0-cost" calls and what you get is very definitely blocking behavior.

main-- avatar Sep 22 '17 17:09 main--

There is a clear cut distinction though. If you do IO in any form that is not deferred to an event loop, that is a blocking call and thus your function is a blocking function.

It becomes the halting problem to try and detect this with a linter.

We could easily do this with an implicit and viral effect system. The issue with is it brings more complications that I don't fully understand. I think this is the way forward though.

mehcode avatar Sep 22 '17 18:09 mehcode

async is basically already the inverse effect, and is already propagated through traits, etc. because it requires changes to function signatures (returning a Future in today's implementation; being an implementation of Generator in Kotlin's implementation and in what I suggested above). This also has the advantage of not polluting "normal" non-async code with anything extra.

This on its own doesn't help with blocking from within an async function, either via IO or just heavy computation. However, lints can pretty easily detect the most common blocking calls, and something like the recent Tokio reform RFC that decouples the event loop thread from the actual future execution can help limit the impact.

Kotlin's approach also benefits here from taking an execution context parameter at all places that initiate async execution. Because you can't just call an async function from a non-async one, and nested async calls inherit the same "executor" (to use futures-rs terminology), this makes it fairly straightforward to manage the issue.

rpjohnst avatar Sep 22 '17 19:09 rpjohnst

@main-- The distinction is between code that is taking your thread a long time to complete, vs code that is taking something else a long time to complete. Making this distinction allows your thread to do more useful work while waiting, and is the reason we're all here.

jtremback avatar Sep 22 '17 20:09 jtremback

@mehcode @jtremback Agreed, there is a distinction - just not a useful one: The whole point of linting against blocking calls in async functions is that you don't want to block the entire application (or more specifically: the event loop). From this perspective, there is no semantic difference between waiting one second for a file read or calculating prime numbers for one second.

The obvious correct solution for I/O is of course async operations. I'm also aware of the reform RFC's thread pool and of course it helps with heavy computation, but if your entire thread pool is tied up with a small number of very expensive computations, this still hangs the entire application. And that's a bug. To correctly solve this, you should not let your thread pool grow indefinitely - that merely masks application bugs with one more layer that's sure to break down under heavy load. Instead, what you need is dedicated worker threads so you can control how much pressure the computation puts onto your system.

main-- avatar Sep 22 '17 22:09 main--

@main-- you're right and you're wrong.

In both cases the event loop and my service is not able to handle the load. However:

  • In an IO bound context:
    • I can scale the service
      • But clock cycles tick, the fan runs, but my expensive CPU is basically useless.
    • There is little I can do other than to move to async.
  • In a CPU bound context:
    • I can scale the service
    • I can try to find efficiencies by improving algos, or architectures.

Async IO doesn't mean infinite scale. Async IO just ensures your hardware is working effectively.

yazaddaruvala avatar Sep 23 '17 00:09 yazaddaruvala

I don't think implied await is always the right thing; you don't necessarily always want to await.

And now to bikeshed a bit here...what the @ (at symbol) might look like, as a unary suffix operator for awaiting, (similar in design to the ? operator):

#[async]
fn fetch_rust_lang(client: hyper::Client) -> io::Result<String> {
    let response = client.get("https://www.rust-lang.org")@?;
    if !response.status().is_success() {
        return Err(io::Error::new(io::ErrorKind::Other, "request failed"))
    }
    let body = response.body().concat()@?;
    let string = String::from_utf8(body)?;
    Ok(string)
}

People were resistant to the ? operator at first, but it became common style...maybe await will be similar; maybe not.

The rationale is similar to the ? unary suffix operator operator. Consider the following contrived JS:

(await (await fetch('/rest/api', {credentials:'include'})).json())[0]

vs await as a @ unary suffix operator:

fetch('/rest/api', {credentials:'include'})@.json()@[0]

gx0r avatar Oct 19 '17 18:10 gx0r

@Grant Minor can we not do that? It would mean people have to another symbol in an already rather complex syntax that is Rust today. Surely typing await vs. @ is not that big of a deal? To me await is far clearer then @, just my opinion.

-- Yours sincerely,

Thomas de Zeeuw https://thomasdezeeuw.nl [email protected]

On 19 Oct 2017, at 20:43, Grant Miner [email protected] wrote:

I don't think implied await is the right thing; you don't necessarily always want to await.

Here's what the @ (at symbol) might look like, as an await operator to parallel the ? operator:

#[async]

fn fetch_rust_lang(client: hyper::Client) -> io::Result<String

{

let response = client.get("https://www.rust-lang.org" )@?;

if !response.status().is_success () {

return Err(io::Error::new(io::ErrorKind::Other, "request failed" )) }

let body = response.body().concat ()@?;

let string = String::from_utf8 (body)?;

Ok (string) }

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

Thomasdezeeuw avatar Oct 19 '17 18:10 Thomasdezeeuw