A final proposal for await syntax

That’s a much clearer justification, thank you.

I personally find it a bit line-noisy, much like @ and #. I can see the argument for it, but I feel that anything that at a glance looks like !word mentally parses as negation, in a way that word!() doesn’t.

2 Likes

Doesn't look like field access is the main one. More broadly: @wait or #await are clearly better than .await because they force, in @160R's words, a “point of confusion” or in my words: an opportunity to learn the control flow feature that isn’t visually lost.

Of course, the prefix syntax also avoids the issue. It seems dysfunctional, to have considered any proposal to move away from prefix keyword, or even the original prefix macro, that doesn't manage to avoid looking like field access. The same arguments are used for dismissing each of the subset of options that are considered under discussion by @nikomatsakis. See his summary, but:

await!(future) (or .await! or .await!())

Its not, or it can't be (implemented in user space as) a macro.

(So? It quacks like a macro and the first form is still available. Can't any diagnostic shortcomings be special-cased and cleaned up?)

await(future) or future.await()

Its not, or can't be (implemented in user space as) a function or method.

(Is that important? It takes a value as arg1 and returns a value. That's usually been written f(value) in rust.)

future.await

Its camouflaged as field access, which should be side-effect free when it isn't, relying only on syntax highlighting to provide a hint otherwise.

(Or why isn't this the logical determination? It seems very hand-wavy and cynical to accept this as the least bad option.)


So @wait or #await aren't the only possible solutions, but they have every advantage claimed for .await and don't manage to hide in the source as field access.

5 Likes

Can you elaborate on this? foo().bar().await.collect() is in the order that things happen ("call foo, call bar, wait for it, then collect it"), so I feel like I'm not understand what you mean by "step by step". And, particularly, are you going through it the same or differently than you would foo().bar()?.collect()?

9 Likes

The starting point is not prefix await. Nor is it the macro. The starting point is that we want the "abstract await operation". The fact that it was initially prototyped as a macro is irrelevant. The fact that most other languages have it as a prefix keyword (not all! Kotlin is implicit await, Haskell is monadic do notation) is not irrelevant, but it's only relevant in what other people do, it doesn't make it a starting position.

I've yet to see support for prefix await other than "this is what other languages do" and "it looks like return <expr>". There at the same time exist consensus limitations to prefix await, such as that transformations of the awaited value (e.g. (await <expr>)? and (await <expr>).context()? are the big two examples) are expected to be common in Rust.

(For what it's worth, I'm personally very weakly in favor of making it look like a method call and putting docs on a "provided magic function" on Future, but I understand the issues that make that a less workable solution. The "point of confusion" argument applies for .await over .await() as well, though, as await can be thought of as functionality of Future.)

So to expound upon the problems:

  • It isn't a macro: All ident!(tokens) macros expand to user-writable code. await doesn't do this. That it was originally prototyped as a macro is extremely limiting to this point, because it cannot expand to even perma-unstable code; the await context is the magic that an await! macro wouldn't have knowledge about.

  • It isn't a function/method: function pointers can't exist as the semantics of await are that it is a resting place for the async statemachine. await has to cooperate with the async transformation to create the control flow with a new "unwinding" channel via drop.

  • It isn't a field: well, actually, given DerefMove, I can make a synchronous version using field syntax:

    struct MyFuture {
        _real: impl Future,
    }
    
    struct AwaitTarget {
        wait: Output,
    }
    
    impl DerefMove for MyFuture {
        type Target = AwaitTarget;
        fn deref_move(self) -> AwaitTarget {
            let res = futures::block_on(self._real);
            AwaitTarget{ wait: res }
        }
    }
    
    fn main() {
        let fut = MyFuture::new();
        let out = fut.wait;
        dbg!(out);
    }
    

(Note: this obviously doesn't work as we don't have DerefMove and I elided a lot of unnecessary detail.)

As said before, .<ident> isn't a pure computation, as it calls Deref::deref.

Benefits of .await over another sigil:

  • Doesn't introduce new sigils to the language

Benefits of another sigil over .await:

  • Doesn't look like a field access

Benefits/Drawbacks of either syntax:

  • Cause a "point of confusion" (await is a keyword, fields can't be keywords; there is no field, traits don't have fields / what's this unfamiliar sigil)
  • Introduce a new syntactic production to the Rust grammar
  • Can potentially be generalized to more general pipelining in the future (not @wait)

Am I missing something (objective)?

Formatting is a big deal in making readable code as well. I don't dislike context .await for when splitting over multiple lines -- it helps keep .await from being lost -- and will happily argue for it being at least an option in rustfmt when it comes to it.

11 Likes
  • The sigil . is accessible in a quite ergonomic position in most keyboards.
  • The sigil . uses The Power of the Dot to facilitate good UX and auto-completion in IDEs.
9 Likes

And, to elaborate on that a bit, as a result doesn't introduce new precedence questions.

Rust doesn't have constructors the same way, so we're not this bad regardless, but if you look at C# you'll see that await new FooProvider().GetFoo() is legal, with await␣ and new␣ having rather different precedences.

8 Likes

So do !, @, #, ...

.await is not a field access. And it does not make sense to auto-complete .await either, it's like auto-completing pub, struct, if, else and other common keywords.

On QWERTY very much not, . is only one row off home row directly under a strong finger, while @ and # are 2 rows off home row above strong fingers and require shift, and ! is two rows off home row above the pinky finger and requires shift (in fact, it's in such a bad position that I even cross columns and use my ring finger for this key). (And on my keyboard layout the difference is even more pronounced, but that's what you get for choosing a layout optimised for current programming languages).

It makes soo much sense to show .await in the autocompletion dropdown in an IDE, when you're in async context the majority of the time you get a Future you will want to just directly .await it (except for the relatively rare times IME where you can run something concurrently). Typing .a? and having it auto-complete the rest of the wait in there would be great. (It was even shown in a previous discussion that Visual Studio using Resharper will show await for completion on a Task despite having to then rewrite it into prefix position).

10 Likes

To further this point, IDEs autocomplete a lot of things (in some cases even typos, other keywords, if statements, function definitions, etc). Their goal is not to be pure, or to be strict, their goal is to be useful.

They're a tool used to make writing code faster/easier, and so an autocomplete being a bit sloppy is a good thing, not bad. The typechecker will error if there are any actual mistakes, so the IDE doesn't need to be strict.

6 Likes

So I have just read the code snippet, and the .await followed by . indeed occurs multiple times, but the only thing that followed the dot in every single occurrence is .context() and nothing else:

// basically every single occurrence ever:
some_expression.await.context("an error message")?;

The above code an be rewritten into prefix without producing concernable noises:

// it has a little bit more noises, but it also makes `await` keyword visible, unlike the dot-chain
(await some_expression).context("an error message")?;

The only situation where .await makes sense is when await is used twice or more and the first future must be used only once.

some_expression
  .await.context("error 1") // this value must not be re-used, otherwise a variable name is needed, rendering dot-await irrelevant
  .some_method()
  .await.context("error 2")

This, I think, is a very niche use-case.

Anyway, my point is:

  • await followed immediately by dot is very common, but postfix await does not solve this any better than prefix await.
  • Prefix await makes some noises, but it makes await keyword visible, even without syntax highlighting. Unlike .await, which hides the keyword in plain sight (without syntax highlighting that is).
  • The only situation where .await is absolutely need is where await is used twice or more in a row (literally), but this is not very common.
6 Likes

You know what else is in the same row as @?

  • & (borrow, reference)
  • ! (macro)
  • (, )
  • _ to ignore unused variable or to infer type
  • = to assign

You know what else also requires shift?

  • & (borrow)
  • ! (macro)
  • (, ), {, }
  • _ to ignore unused variable or to infer type
  • " (string)

These are more common than await, and just as common as (if not more common than) field access.

Adding to this, programming is not just sitting and writing code, the majority of time you spend are on reading, thinking and analyzing.

This particular argument is a very weak argument.

minor things that I would like to not discuss any futher as my concern is primary await syntax

Well, you convinced me that auto-completing await does make sense.

I'm not sure what do you mean by this ("sloppy" in particular). But I have had troubles with VS Code displaying too many things including those that don't make sense, and obscuring things I want as a result.

Case in point, RLS is very "sloppy", perhaps too sloppy: It automatically write out argument names and type signatures into the editor, forcing me to remove them (not by simply pressing del, but by selecting the whole thing using mouse and pressing the delete button) and write actual argument into it.

My point is, being sloppy is not "good" in any shape or form.

2 Likes

I agree that it's a trivial argument that doesn't really increase the weighting towards . over other symbols, I just refute the fact that the same argument applies to !, @, and #; . is objectively more ergonomic than them.

4 Likes

Note, that @ is already used for pattern bindings, so strictly speaking @await does not introduce a new sigil, but extends functionality of the existing one, i.e. in this regard it's fully equivalent to .await.

5 Likes

I never used the term "ergonomic" as I never really understand what it means. Can you explain it to me? Is it easier to read, easier to write, or something else?

Again, I've never used the term "ergonomic", I prefer a simpler, more straightforward term ("readability", "explicit", etc.). And all of my comments above had already talked about them (how and why dot-wait is bad for readability, etc.).

Anyway, you said that you refute the fact that the alternatives being "more ergonomic". I said they don't hide await in plain sight like dot-await does, and thus, they are more readable.


Update

So there's a guy that just told me that "ergonomic" means "comfort" via a private message (instead of just replying here so that I don't waste time explaining). But I still don't know if it is "comfort to write" ("easier to write") or "comfort to read" ("easier to read"). Assuming it means "easier to write", then I'd say that I value "easier to read" (readability) more (i.e. prefix await).

1 Like

I have similar experiences reading all the examples. If I happen to notice I'm looking at an async function, my brain switches from "scan for the usual control flow" to "scan for usual control flow, and scan all expressions for postfix await".

Then again, I also still have issues even recognizing I'm in an async example. I will need to train my brain to even expect it there and scan for the async before the fn. This is more of a problem with complex signatures of course.

I agree ? is control flow just like .await, and as such one needs to know where it is, that is correct. I'll personally prefer to wrap my awaits in prefix macros for my own stuff because:

  • I can design my non-async code with RAII guards so a ? or panic or other macro wrapped control flow will work as I expect it, and release all possible resources.
  • As I understand it, await will keep things in scope alive. So I need to know more to be aware whats "live".

So in a way, for me having something recognizable on the head of the statement is more important for any form of await than it is for things like return. Because my internal watch-out circuit needs to switch from "make sure resources and state are properly RAII'd" to "make sure to not accidentally hold on to resources because of RAII." Which is kind of a reversal in spirit. I haven't followed closely enough, but I assume the same will be true for temporary values in statements and long chains.

These kinds of things are why I find visibility in plain-text diffs without highlighting to be important.

Edit: I'm also more careful these days with ? and try not to overdo it. In many cases it's a form of statement terminator for me these days.

6 Likes

The “don’t hold onto a RAII guard too long” problem exists in non-async threaded code as well, though. If you call some blocking method unrelated to a lock you’re holding, then you’re incorrectly starving that lock. It’s the exact problem with .await when holding an unrelated lock.

RAII is still the (only?) correct way to hold onto resources in async contexts, as it makes it both panic and cancel safe, where cancellation is unique to async contexts.

6 Likes

But in that case, the whole call-chain will be blocking. Before the caller will continue its execution, the locks will be released. It's serialized versus non-serialized. It might slow down other threads waiting for things,

I also think there are other reasons why this is less of an issue in true parallel running code, but it's hard to verbalize. If I use other threads it's either extremely local (like parallel iteration) or very big picture (dedicated worker threads) where containment of logic is easier.

But even in those cases, blocking behavior should be obvious and needs to be kept in mind. It's not that without prefix await I wouldn't have to worry. It just makes me have to worry about more things in more places.

RAII is still the (only?) correct way to hold onto resources in async contexts, as it makes it both panic and cancel safe, where cancellation is unique to async contexts.

Well, it's the basic mechanism. But it means there will need to be change to how state is handled. It would be my expectation that introducing a new await (in any form) into a function will lead to having to make changes in the surrounding code more often than introducing a new ?. Because with ? and return, all function local state is discarded.

1 Like

I take it you're not familiar with Perl 6, then? (The process for development was very committee-driven, and the final language includes many non-ASCII symbols, though their usage is optional.)

1 Like

This holds in async code if you await as well; it's the other (green) threads of execution you have to worry about. (A poll and forget could lead to forgetting a lock guard, but that seems like a degenerate case; poll and drop wouldn't.)

Then I don't see the issue with just considering .await as another potentially (task) blocking call. It's just as visible as any other blocking method call.

This definitely makes it easier to work with, as the RAII guards are dropped. .await is closer in consideration to a new blocking call in a task (green) thread pool than a return, both in semantics (potentially modulo poll) and "danger" cases around RAII.

2 Likes

Well, it's more that I have to worry about all of them :slight_smile:

That's what I'll do, but I still try to have those stand out. Thus, some kind of await_until!(...) prefix variant, or hopefully at least having .await or .await? on a separate line as terminator of the statement. I can't see myself putting it at the end (edit: of the line) or inside a long chain like ?.

Honestly not sure if we even disagree on anything here :slight_smile: After all, it's about the difference between ? usage and awaiting.

Exactly! Both blocking of potentially shared resources and stopping execution of the current thread, and holding resources while letting other code run on, need a certain amount of care. I wouldn't want to have hard to see blocking calls either, for example by hiding the blocking action deep inside some chain. I'd want that dedicated and standing out.

1 Like