Await Syntax Discussion Summary

Regarding parsing complexity there are two remarks:

Field access and method call are not competely equivalent in the language with regards the operator precedence. The difference is minute and occurs when the Output of the futures is to be used as a function (i.e. Output: FnOnce() -> usize for example).

  • under await method chaining as expected: future.await()()
  • with field await paranthesis are necessary: (future.await)()

This doesn’t look like a clear argument either way to me (both have a tradeoff) but it is maybe interesting and subtle enough to note it here for clarity.

Additionally, method call await could lose all of its magic of ‘where does this function come from’ because methods appearing on a type already have a place in Rust: traits. In fact, await is eerily similar to a trait like this:

trait Await {
    type Output;
    #[lang = "await"] // where `await-call` marks a new, imaginary call syntax
    extern "await-call" fn await(self) -> Self::Output;
}

So much, in fact, that I wonder if it could not only be conceptualized that way but also actually implemented. Requiring no parsing changes and also serving as a single point of documentation that fits into all current documentation tools as well, is discoverable by default, etc.

7 Likes

It looks like that thread was never updated. Was this discussed at the meeting?

3 Likes

It was. We also discussed and reached consensus about a path to resolving await syntax as a whole, which I have created a separate thread for. As to for await syntax in particular, we decided that:

  1. We are not certain we want this syntax at all for a couple of reasons.
  2. If we do, we think we likely can make it not a separate syntax, but just treat streams as processable by for loops as a streaming iterator of futures.
  3. No matter what, we think there are acceptable fallbacks and this won’t box us into a corner, so we don’t need to resolve this yet. That is, it doesn’t block the initial await syntax decision.
6 Likes

After reading the paper, I have to say I’m a pretty strong proponent of postfix foo.await!(), but that is probably because I am a fan of macro-methods in general. I would love to be able to attach macros to modules mod.macro!() and as at least “static functions” on classes Foo::macro!(), if for no other reason than that it could help organize them.

I’m not sure about the lang difficulties of generalizing a feature like macro methods, whereby the macro “takes in” the $self:expr, but I am strongly in favor of having this ability as I believe it could open up some crazy-cool orthogonal features for the rest of the language!

Some minor points on “weirdness budget”:

  • foo.await: Rust doesn’t have property based access (whereby code is executed when accessing properties) and this would effectively add that. Personally I feel that property access obfuscates code and makes it hard to read. Obviously this is less an issue with a keyword, but I expect that if something “does something” it should have () at the end.
  • foo.await(): better, but because it is actually modifying control flow, I feel the macro syntax is better.
  • other syntaxes: I suppose opening up a completely new syntax (like pipe ->) may be valid and haven’t put a lot of thought into it. Personally, I would want it to be part of a new orthogonal design space, not a one-off.

I guess an important point is: if we went with “macro-method”, could it be written in such a way that other macro-methods could be written behind a feature-flag, to make sure that it was a generalizable feature?

2 Likes

I’ve read the various docs which have been posted and I’ve skimmed this thread, and it still feels to me like this decision is missing the forest for the trees. I agree that writing (await foo())? and (await foo()).bar() is a little un-ergonomic and noisy, but it’s much, much more intuitive to me than foo().await, in that the former follows the general convention of keywords applying to the entire expression following them. Thus I feel the proposed solution here is worse than the problem it is trying to solve. Furthermore, I don’t really understand why this is considered to be blocking for async / await—we had try!() for years before we got ?, and it feels like we could do the same thing with await!() here while more discussion occurs on our preferred non-macro await syntax. Not doing so seems a little rushed, which feels out-of-character for the lang team which seems generally conservative.

It’s possible I’ve missed some discussion which clarifies these points, in which case I apologize for bringing them back up.

6 Likes

This has been said many times. We don’t want to support a syntax that we know that we are going to deprecate within 1-2 release cycles. So a tempoary await!(...) is off the table. This is different from try!(...) because with that, there was no indication that we wanted the ? syntax. Here we know that we are going to get a new syntax and soon, having more discussion is not going to move this forward substantially, so deciding on a syntax now is the best way forward.

4 Likes

That’s understandable, thank you for clearing that up @Yato. That said, I still feel as though not enough consideration has been given to (await foo())?. In the discussion I’ve seen so far, (await foo())? is immediately dismissed for what seem to be comparatively minor ergonomics reasons. I’d like to see some discussion of why introducing new keyword behavior with .await is considered preferable to having users type a couple more parentheses. Perhaps I’m not fully understanding the costs of (await foo())?, since the general consensus seems to be that it is untenable.

7 Likes

If you haven’t seen it yet, there is a new post about the lang teams meeting last week where new discussion is happening


I don’t have much of a stake in async/await so any syntax is fine with me. I prefer the post-fix syntax because it works well with our existing features, like ?.

3 Likes

As I understand it from @withoutboat’s blog, prefix await isn’t out of the running officially, it’s just expected to lose when the final syntax decision is put to a vote.

Given how contentious the proposals are, why would we be certain that the macro would be deprecated “soon”, much less “within 1-2 release cycles”?

3 Likes

Assuming I have a value val: Pin<P> where P::Target: Future but not P::Target: Unpin. Am I supposed to be able to await that pinned future?

Looking at the structure of the current macro, the only difference is that the pinning has already been performed. Still, since Pin<P> is not itself impl Future, it will not work. Is this an intentional restriction?

If not, this has some bearing on the semantical discussion. For a field based syntax, or other syntax that magically works solely on impl Future, this will either never be possible (e.g. val.await shouldn’t work) or be another magic special case. But, for a trait based solution (trait Await, see above) such an extension could be introduced naturally later on for extra awaitable types within the standard library. With the restriction that they must not be impl Future so that such an impl is unique. Which would, in an interesting and useful quirk, also automatically disallow implementing that trait for the user.

(I had previously unintentionally posted this in the wrong thread, sorry)

impl Future for Pin<impl Deref<Target = impl Future> + DerefMut + Unpin> already exists (link not guaranteed to precisely work if the number/order of impls on that page changes).

1 Like

Because discussion is most likely going to stall over the course of 6 weeks and that will lead the lang team to make a decision to prevent blocking asyc/await any longer. Also, thd entire point of asyc/await is more ergonomics and better diagnostics, and using the await macro wouldn’t give good diagnostics, so it doesn’t work.

I don’t understand your first point. Stabilizing the macro would mean that async/await wouldn’t be blocked any more.

1 Like

Ah, yes that is true! :sweat_smile:

1 Like

I think the “answer” is “don’t pre pin it”. Since Pin<P<impl Future + !Unpin>> roughly would mean “started future”, I personally fail to see when awaitting it elsewhere would be correct. (In fact, it’s incorrect if it’s being awaited elsewhere, as one of the two await points would thus poll it after finishing.)

Hi all,

I’ve just read the summary and I would agree about the convenience of the prefix solution. I am far to be an expert in programming language design but I’d like to contribute with a humble suggestion: given that the dot keyword syntax conflicts with field access, how about replacing the dot by a question mark?

The syntax would be something like the following:

foo()?await.bar()

I think this would be congruent with the ? keyword already added to the language: the new keyword would work in a similar way and it can be understood as an extension or special case of the former one as well.

The new keyword ?await would manage any result returned by the precedent expression the same time it awaits its future execution .

I think this would be more convenient than using .await.

This is not a good idea because it ties together awaiting and error handling. What if we later add a way to unwrap generically. Example syntax below (I know this syntax is ambigious).

let x = res!; // unwrap the result

Would we also need to add !await so taht we can unwrap the result and then await after? This doesn’t solve the problem, and is no better than prefix await? as seen in the previous thread.

Now if you mean that ? and await would be separate things. Then this is the same proposal as future await, which the lang team discussed in this post.

I meant I would agree about the convenience of the suffix solution

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