Await Syntax Discussion Summary

This is probably subjective, but the last example looks like a script to me, maybe a bit like Perl (which I don’t know much of, but to me represents a symbolful language).

Understanding the last example seems easier once you know what ? and # (maybe even knowing only one of them is enough to guess the other). But on the other hand, not knowing both seems to be the worst case here. Clearly ? is not the ternary operator and # is not a comment marker, but whatever they do affecting the control flow would be the most surprising to me. I think this is what the “weirdness budget” is.

On a prefix with no sugar syntax it could look like this:

async fn example(db: DB, hash: Sha256Hash) -> Result<String, Error> {
    let response = (await get("rust-lang.org"))?;
    let original_body = (await response.body)?
    let body = await add_footer(original_body);
    let db_operation = db.store_body_if_hash_eq(&body, hash)?;
    (await db_operation)?;
    body
}

Which is not worse IMO.

7 Likes

First of all, great write up. I’ve been following the discussion and can tell this must have been a long road given all of the nuance of the design. As such I’ll try to be brief;

In my eyes, it’s pretty clear that foo.await() is the way to go. The syntax “just works” and the problems of familiarity and operation ordering are already solved. Everyone knows the syntax because it’s just usual invocation syntax. It has the added benefit that we don’t need to add an await keyword at all, because it’s just a definition on the Future type. This already exists in foo.wait() in the external futures crate and even when I was a beginner it was obvious as to what this did. Of course the internals may be a little different, but the concept is still the same.

I agree of course that none of the listed options are perfect, but I have always loved Rust specifically because it’s easy to see what’s happening. Adding symbols and/or keywords when they’re unnecessary would be a shame, especially in a case where simple function syntax will suffice. Some cases make sense (such as ?) when they’re extremely general, concise, and there’s a logical symbol for the sugar. Although this case is very general, I don’t believe we’ll be any more concise than foo.await(), and even if we tried to pick a symbol for it I don’t know how we would determine a “good” choice without further debate.

17 Likes

When discussing await syntax, do we consider that it should be applied to expressions rather than to values?

P.s. Using async as keyword in postfix operations is not a way to go considering majority of keywords are applied as prefixed

2 Likes

Great write up! Coming from a typescript background, I gotta say I find the prefix await foo() syntax much more comfortable. But the error handling dilemma does make a strong case for postfix foo().await().

Why not support both? I don’t see any reason the two couldn’t coexist. There are some cases where it could make more sense to use the prefix syntax and I’m sure having it there would make new users more comfortable with the language.

I also strongly hope they do not go the postfix foo.await field access route. That seems to contradict the orthogonality argument, as I would not expect a field accessor to modify the behavior of my program.

AFAICT, these are the main cons for the postfix options:

  • fut.await: Too much like field access.
  • fut.await(): Too much like a method call.
  • fut# / fut#await: Wierd.
  • fut.await!(): Requires a more general feature.

The only thing that I feel strongly about is that fut.await and fut.await() are bad. It adds cognitive overhead to differentiate between “look up a struct member”/“call a method” and “perform an await”. All three operations are distinct and should have distinct syntax. The problem is worst for users who prefer minimal or no syntax highlighting.

On the other hand, the postfix macro fut.await!() is a cool idea. The ! resolves the cognitive dissonance between an await operation and a method call. And postfix macros would be a powerful and orthogonal feature to introduce, regardless of async/await.

I think all of the pros of the attribute and method syntaxes apply equally to the postfix macro. Postfix macros avoid my biggest concern with attribute and method syntaxes. Postfix macros are a generally useful feature.

The syntax to actually define a postfix macro is the hardest part, but perhaps something like this could work:

postfix_macro<T>! await for Future<T> {
    () => {
        await!(self)
    }
}

That would mean that a postfix macro, generic over T, called await is defined for all expressions of type Future<T>. The macro takes nothing within it’s token tree and expands to the expression await!(self) where self is the recieving expression.

Edit: I just realized that this would be difficult because macros are expanded before the type check (I think). This kind of postfix macro would probably add serious complexity to the compiler. That said, I still like it from a language POV.

14 Likes

Please keep it on topic

Feel free to discuss the await syntaxes brought up in the original post, and relevant related topics. This is not the place to discuss the necessity of async/await in rust itself.

8 Likes

Random opinionated redditor here.

For postfix await, I feel it’s important to consider that the English verb “await” precedes its object (as in "this function will now await the computation of "), so without something to help it more strongly associate to the left, you’ll be paying a bit of Weirdness Budget for conflict with human language.

For that reason, I personally think that space-await is a poor fit. dot-await is fine, as it has the dot to join it to the correct expression, except that it’s on par with space-await when dot-await-dot makes it symmetrical again. “.await()”, then, is good, but now you have the magical special case of The Only Function That Can Tamper With Its Caller’s Control Flow. So in comes !, already a familiar herald of shenanigans. For postfix await, I personally like "@await ", "@await! ", ".await! ", and “.await!()”, each having either a space or parentheses on the right to separate it from following code, something on the left to connect it despite English’s bias, and any symbol that hints it’s more than just a field or method.

6 Likes

There is an option I think is being overlooked. "await!". fut.await! is not easily confused with anything else. It can't be a member or a method call. It also looks good when combined with error handling: let x = future.await!?

12 Likes

let x = future.await‽? :stuck_out_tongue:

6 Likes

Let me first say that the writeup was very well done and very concise. And before reading it I was very much in the prefix camp with standard binding principles (namely use brackets) as it would be the path of least surprise.

However, I am now thoroughly convinced that a postfix form is practically required. So I would propose that both are added.

Have the await prefix for teaching and least surprise and then the sigal @ for conciseness and fixing the postfix problem.

2 Likes

Just a note, but; I disagree with classifying “call a method” and “perform an await” as separate flows. I’m not convinced there’s any conceptual difference here, especially one which needs special syntax to demonstrate. When you see code calling a method, it signifies that something is being run which can give you something back when it’s done - this is no different to the general concept of awaiting a Future.

To put that slightly different; if you were to try to lock a Mutex, it could potentially be a while before you eventually receive a lock. I’d argue that this is conceptually no different to awaiting a Future (to the developer). Locking a Mutex and awaiting a Future looking different under the hood does not require you change how you interact with them.

12 Likes

Yes.

When I started reading the summary, I was convinced that prefixing was the way to go, but by the end I changed my mind. The arguments in favor of postfixing are very compelling. The drawbacks mentioned are very severe, but they will fade with time.

A reasonable solution is to intentionally delay. There is nothing wrong with await!(fut) for now. It's just a little verbose. We could do far worse.

In a few years, once everyone uses async/await every day, there will be a push for sugar. Then something like a # sigil or other postfix which today could be ruled out for being too disruptive will be a welcome improvement.

9 Likes

I'd consider this: in math, we declare sin²x means (sin x)², or squared of sine, while sin x² or sometimes with explict brackets sin(x²) means sine of squared. In this way I personally may prefer obvious precedence, where question mark plays a similiar role as the square mark. For example:

sin²x ↔ await? future

sin x² ↔ await future?

More complex statements are also possible:

sin sin x ↔ await await future

(x²)² ↔ (result?)?

sin²(sinx) ↔ await? await future

sin sin²x ↔ await await? future

sin sin x² ↔ await await future?

By this way async/await syntax could be more user friendly. In further tutorials authors may compare syntax with math so that new Rust programmers could understand them better.

6 Likes

Are there any concrete examples of what “prefix with syntactic sugar” would look like?

On the strangeness budget: I'll note that things moving to more-syntactically-convenient locations may be becoming less strange. For example, C# 8 switch expressions moves from switch (o) { ... to o switch { ... to, among other things, make it "more compositional".

That sounds very reminiscent to me of the advantage cited here by the postfix camp of natural parenthesis-less composition.

I think this is an important point that I couldn't find in the paper doc. :+1:

(It's a notable difference from ?, which doesn't have a reminder-marker on scopes that might contain it -- and that may have contributed to the initial concern about the subtlety of ?.)

I'll note that we actually have "if you don't know it's a keyword it could be something else" already. For example, if you see foo { x }, it could be a struct literal (if foo is a struct foo { x: i32 }), but it could also be a return expression (if foo is return). For that matter it could even be loop { x } (if x: () or x: !). Also, parens allow things like func(c) and return(c).

Now, admittedly there are some casing conventions and lints that help make those clearer. But in this case you have the extra forewarning of async that it's going to happen.

So I'd find the confusion argument more persuasive with arguments about why this theoretical ambiguity is worse than the other ones. Especially since I find even the very-simple "embolded this list of keywords" highlighting makes it very clear. (Try it with match, for example.)

That leads to style guide discussions, to your mental pattern matching having trouble on other people's code, to questions about whether rustfix should normalize to one, etc. I think I'd rather have the one I like less than have both.

16 Likes

Do I really need to have a Dropbox account to read the writeup, or is the link broken?

11 Likes

I am a bit disappointed that postfix-sigil option got excluded so easily. Personally I strongly dislike .await and I was surprised to learn that it has the greatest level of support in the lang team. I think it is obviously not orthogonal and thus will noticeably drain strangeness budget. If you really want await to be present, then why not introduce an additional operator to be used instead of . for situations like this? For example:

// it was proposed several times in the previous discussion
foo()?
    @await
    .bar()
    @await?; 
// looks a bit weird, but will utilize "`!` is special" intuition
foo()?
    !await
    .bar()
    !await?; 

And later it will be possible to generalize this feature (maybe for postfix macros?).

@dlukes

The link has worked for me previously, but now it requires registration for some reason…

1 Like

I am also persuaded by the rationale for postfix syntax.

I would also like to add my voice to the chorus saying to please add something, anything, to distinguish awaiting from field and method access syntax. Rust has a strong philosophy of making everything explicit, which is wonderful because it means I don’t have to concentrate on these kinds of issues when reading/writing Rust, and can spend my mental energy on the actual task.

I still favour postfix macro syntax .await!(), as I believe that this could lead to a very clear and consistent syntax if this is later developed into a more general feature (and it also works well if a general postfix macro feature is not developed). And macro syntax already conveys the idea that “this does something non-trivial”. However, @await, .await!, !await, etc would all be acceptable to me. I just believe that it is important that there is something that marks the await syntax as being different from normal field or method access.

7 Likes

+1 to that; furthermore, from what I can tell, the orthogonality argument doesn't really address plain prefix await, only await?. Since awaiting is a semantically important operation (and definitely not a field access), I'm still strongly in favor of prefix await (with either optional or mandatory delimiters, although optional delimiters would be more consistent with the syntax of if and while). Having to parenthesize an expression is really not much of a disadvantage; it happens elsewhere too. I don't think that the primary goal should be getting rid of as many parens as possible, especially not at the expense of adding more magic. Incidentally, using field access syntax for control flow operations also introduces (arguably worse) non-orthogonality.

3 Likes

Would it be easy to implement postfix-sigil await as a [procedural] macro in external crate regardless of which syntax is decided here?


use await_sigil::await_sigil;

#[await_sigil]
async fn q() -> Result<()> {
    let x = something()?~?;
    x.some_method()~?;
}
2 Likes