A final proposal for await syntax

tradeoffs of postfix dot notations

I’ve been reading this thread. I want to try and leave a few thoughts. I’m going to write two comments in short succession. This first one aims to give a more detailed look at the various “postfix options” that are based on .. It begins by summarizing the key points around each option, and then gives me “personal narrative” that explains where I personally fall.

I want to emphasize that I am explicitly not considering future generalizations in this post. I’ll talk a bit about the reasoning in the next post, but the bottom line is that “you never know what may come”, so I’d like to reason about “what would happen if we had this syntax forever”. (But also, in the next post, with an eye towards future possibilities.)

Also, I apologize for the length of this post. If you prefer, it is also available on HackMd.

Go forth and experiment!

One thing I want to emphasize is that I think we are could use fewer comments in this thread and more lived experience. We’ve seen a lot of fragments of code, but those fragments were written using await!(). Now that .await syntax has landed, I would really like to see people go ahead and use it! Give yourself a little time to get used to it, but also pay attention to moments of confusion or surprise. If you can give concrete examples, that’d be helpful. This is the point where .await will feel the worst, since we don’t yet have syntax highlighting etc, so that should be a good torture test. I’d be quite interested to hear those reports.

Finally, people have brought up screen readers and accessibility. I do not understand how foo.await might be a problem there, given that we already have field and method syntax, but perhaps others can clarify that (I’d particularly like to hear from people who are actually using screen readers).

Feedback welcome

As always, I’m interested in getting feedback – but of a particular kind. I am looking for arguments (pros/cons) that you think are not included in this summary. If I see anything that seems new, I will add it to the summary – and, hey, maybe you’ll change my mind. =)

NB: I will update the HackMd version of this post, not this comment.

Executive summary

  • foo.await?
    • Pro: very lightweight
    • Con: easily confused for a field access, which conveys a “no side effects” intuition
  • foo.await()?
    • Pro: good analogy to blocking I/O; functions mean “something happens here”
    • Con: could mislead into thinking other methods could not block
    • Con: sort of strange to have a keyword with extra characters () “just because”
  • foo.await!()?
    • Pro: macros have always had ability to do surprising control flow
    • Con: very verbose – foo.await!()?
    • Con: not a macro, could never be
  • foo.await!?
    • Pro: fairly lightweight, clearly not a field
    • Con: could mislead into thinking ! is an operator (indeed, it is, for macros)
    • Con: no precedent for this sort of thing in Rust “syntactic tradition”
    • Amusing: means foo.await!? is a thing, for better or worse
      • But note that foo.await?! would not work – I actually made this typo a few times, this could be pretty annoying in practice. =)

Running example

To help make each syntax more real, I want to use this “running example” (expressed in naive prefix form). These ‘snippets’ are adapted from the await syntax repo:

let ids = (await interface_ids(service.clone(), interface_id))?;
 
let response = (await wlan.list_interfaces()).context("error listing ifaces")?;

.await

let ids = interface_ids(service.clone(), interface_id).await?;
 
let response = await wlan.list_interfaces().await.context("error listing ifaces")?;

The main concern about foo.await, from what I can tell, boils down to the fact that it will be confused for a field. This seems to offer the biggest possible “mismatch” between user expectations of the available options. In general, there is a sort of “spectrum” of side-effects one might consider from a given bit of syntax, and field accesses fall on the lowest possible side of that. Thus, making foo.await potentially block the current task and switch to another is potentially very surprising. (This is also an argument against progammable properties, which of course Rust doesn’t have.)

In Rust in particular, the “field vs operator” distinction can be significant in other ways. Method calls return temporaries, for example, so while &foo.bar returns a value that lives as long as foo, &foo.bar() creates a temporar that lives as long as the enclosing statement (typically). &foo.await would be the latter, which is unfortunate.

Clearly, syntax highlighting will help to avoid confusion. I think this is very important, but I can see counter arguments: e.g., there are contexts where it doesn’t apply: for example, when I write foo.await in markdown, I don’t get highlighting. Similarly, syntax highlighters often highlight keywords in incorrect contexts or miss things, so maybe users don’t trust this entirely.

It is worth asking how much trouble there will actually be a result of this confusion. I suspect that, most of the time, .await'ing a future will not, in fact, have visible side-effects on the data you have in hand, although it clearly can.

(In general, one of the big advantages of explicit await is that it lets you ignore async I/O most of the time, but when you need to care, you can. That is, when you need to audit for where side-effects occur, you can easily find them. This is similar to how ? lets you ignore errors a lot of the time, while making them visible. Field syntax doesn’t make it harder to audit, but it probably does cause those awaits to fade even further into the background than they otherwise might.)

.await()

let ids = interface_ids(service.clone(), interface_id).await()?;
 
let response = await wlan.list_interfaces().await().context("error listing ifaces")?;

One obvious answer to “fields accesses don’t have side effects” is to use a syntax that conveys the notion of side-effects. We could, for example, use await().

There is some danger here, though: this is not an ordinary method. It is an operator, despite looking like a method. It could lead to the “inverse” confusion – that is, that any method call may potentially block, instead of only this “special method” called await.

Similarly, writing .await() as a function may make people “think of it” as a method, and thus be surprised that they can’t use (e.g.) some fully qualified syntax like Await::await(foo) to invoke it without dot notation.

Ultimately, I think that the .await() syntax is leaning very hard on the “you can think of async I/O as if it were blocking” intution very hard. In other words, you can think of an async fn as running in its own thread, with .await() corresponding to a blocking operation where the scheduler may choose to run another thread. In that case, the analogy is basically perfect, even though the model is not at all what is happening. (I find that somewhat appealing, myself, but you can also see where it might be misleading.)

I personally find await() a bit surprising for other reasons: Rust has a kind of tradition of “bare keywords”, and somehow adding two extra parentheses “just because” feels surprising to me. But I think that’s a somewhat weak argument. (We do have unsafe { } blocks, which are somewhat similar.)

.await!()?

let ids = interface_ids(service.clone(), interface_id).await!()?;
 
let response = await wlan.list_interfaces().await!().context("error listing ifaces")?;

Certainly macros are expected to mean “this expands to unconventional things” – so we could imagine using !() to convey that in this case.

I think one very strong argument against this is just verbosity. Even with just .await, async I/O is already far less ergonomic than sync I/O – adding !() feels to me like a bridge too far.

Of course, we do use macros for a number of common purposes, such as println!, panic! and so forth. Indeed, those used to be keywords, but were made into macros to simplify the language and compiler. But here we don’t get that benefit: async/await is still a core language concept; it also must still be implemented in the compiler.

Separately, of course, it’d be nice to have general method macros at some point, but I don’t see that as really affecting this question very much. If we choose to add method macros, it would not conflict with the existing of a .await keyword in particular.

.await!?

let ids = interface_ids(service.clone(), interface_id).await!?;
 
let response = await wlan.list_interfaces().await!.context("error listing ifaces")?;

One answer to the concerns with !() question is to just use !. This builds on the "! means this may have unconventional control flow" intution, but without being overwhelming or looking like a “normal” macro.

It does mean that you would commonly see stuff like let x = foo.await!?, which is … kind of amusing. Still, it’s worth noting that foo.await?! would not work (it’s meaningless) – but normally when one combines ? and ! it is done like so “What on earth?!!!”". My fingers at least mistyped this quite a few times. =)

Still, while Ruby and Lisp have identifiers like foo? and bar!, there isn’t much precedent for this in Rust, where ! is used a an operator (though in ways that don’t, I don’t think, conflict with this syntax):

  • !foo of course;
  • but also $a!(..) in a macro, which invokes whatever macro name $a expands to.

My personal narrative

The previous sections were meant to be fairly dispassionate (if not truly “objective”). This section tries to explain why I personally lean towards foo.await (for the purposes of this comment, I am ignoring future compatibility).

I think the bottom line for me is that foo.await is the least intrusive syntax. When writing Async I/O code, I imagine one has to do a lot of awaiting, and most of the time you don’t want to think about it very much. I think people will learn quickly that it is, indeed, not a field, but rather an operator, and I think that syntax highlighting will help. I am also influenced by the fact that @cramertj, who has written a ton of actual async-await code in Rust, prefers .await.

I think that of the other options, I would choose foo.await() as a second choice. I don’t mind that it “looks like a method”, because I think that – in terms of its potential affects – await can be thought of as a “blocking method”. My main hesitation here is that, ultimately, a keyword suffices to remove ambiguity, so the the () is just there to “signal” to the user that this is not a field. But maybe that’s important enough to be worthwhile. (And, of course, there is no future generalization path here, but I’m trying to ignore that.)

I could live with foo.await! because it’s short, but ultimately it feels like more of a departure from our syntax to me.

I personally find foo.await!() to be too verbose and do not consider it a contender. I realize others disagree.

Feedback welcome

Let me just repeat what I said before:

As always, I’m interested in getting feedback – but of a particular kind. I am looking for arguments (pros/cons) that you think are not included in this summary. If I see anything that seems new, I will add it to the summary – and, hey, maybe you’ll change my mind. =)

NB: I will update the HackMd version of this post, not this comment.

30 Likes