A final proposal for await syntax

If space-await were kept fully consistent with as then this wouldn't be an issue, even though a . cannot be part of a path as doesn't currently allow following the RHS path with a method so the parentheses are necessary:

error: expected one of `!`, `(`, `::`, `;`, `<`, or `}`, found `.`
  |
  |     5u32 as i32.abs()
  |                ^ expected one of `!`, `(`, `::`, `;`, `<`, or `}` here
1 Like

In general, I’m happy with the decision for a postfix approach.

That said, I wonder if future.await() wouldn’t have been a safer choice, for the following reasons:

  • It’d be straightforward to allow parameters later on, e.g., an explicit waker or executor
  • The syntax future.await (i.e., without parameters) could then allow us to refer to the generated closure, without actually calling it.

It would also IMHO be easier to teach, as a “coroutine call”, which essentially it is.

If it turns out, we never add a parameter, or refer to the closure, then allowing the parentheses to be dropped would be a comparatively simple option (while adding parentheses afterward’s wouldn’t be).

1 Like

Just a small thought, and taking a small step aside… what about .then value { ... } in addition to .await?

Ah, this is a good point. We don’t have to worry about that then. We could still allow the programmer to use the ? operator on it since other operators can be used immediately afterward, so we wouldn’t have to put parentheses everywhere.

x.async_operation() await?;
x.async_operation()? await;

I guess that raises the question of why we would end up allowing a dot after the question mark in the above scenario, but that seems like a solvable discussion.

x.async_operation await?.next_method();

That doesn’t seem terrible to me. I still like it because a new programmer doesn’t need to know that it exists in order to avoid it.

1 Like

await is a keyword anyway so it's still impossible to call a field or function await. It's not necessary to know or remember this because the compiler will remind you. :slight_smile:

3 Likes

I don't think the total number of comments is quite fair as a metric for how much "noise" is being generated without new arguments, because a lot of the comments are clearly not written with the intent of changing the team's mind.

I do, however, think that several points made so far seem fairly novel. I'll quote them here without introducing my opinions on whether they're valid or important; they're merely included because they weren't mentioned in the team's write-up or in the previous internals thread.

.await makes + non-commutative in some cases:

.await() should not be dismissed as "not a method":

There is some circumstantial evidence that the community may feel strongly against this proposal, which may warrant additional outreach and research:

Chaining may actually be detrimental to clarity:

.await may be detrimental to accessibility:

There is a proposal for a generalized "postfix (or infix) keyword" syntax that would not apply to .await:

An argument for .await that wasn't in the write-up:

5 Likes
"test.txt" File::open?

Rust is not Forth.

2 Likes

I don't see how this is .await-specific. (await foo) + await bar and (await bar) + await foo are also different.

(Adding parens there because I don't intuitively know what the relative precedence of prefix await and infix addition are.)

I've heard both of these in various forms, but not why await is substantially different from ?, which is also about "the control flow of the code" and whether you "go into the expression knowing" there's a rethrow point.

Personally, my default is to apply higher weight to arguments that also argue for previously-accepted things and lower weight to arguments that argue against previously-accepted things.

This was in the original write-up:

5 Likes

I would like to point out that there is no guarentee that + is commutative in general, and in some cases will not be, for example string concatenation. So the objection that await makes + non-commutative is not well founded.

7 Likes

This comment seems to be of satirical nature but I can honestly imagine using it sometimes if such postfix function application had an appropriate syntax. For the moment, the best comparison where this is subjectively more readable. Imagine opening an adjacent file:

// Instead of:
File::open(path
    .parent()
    .ok_or(Error::NoSuchFile)?
    .join("actual_file.txt"))?
// vs.
path
    .parent()
    .ok_or(Error::NoSuchFile)?
    .join("actual_file")
    $(File::open)?

Or for constructing the inner value of some newtype struct Wrapper(Inner)

let value = Wrapper(Inner::new()
    .configure()?
    .some_more());
// vs.
let value = Inner::new()
    .configure()?
    .some_more()
    $(Wrapper);

Or for freestanding functions that can not be member functions because the argument type is from another crate, and a whole trait with import is a lot of overhead.

Of course, you could introduce temporary bindings but sometimes... It's that little bit cleaner. Not with the ad-hoc syntax up there of course.

Edit: Any further discussion on this should be in another thread though

4 Likes

And one other bit of new information raised in this thread I haven’t seen elsewhere: "space await" binding like as rather than like "dot await".

The reason this is interesting is that one of the main arguments against "space await" is that it visually groups wrong when you do future await.context(_)?. If this had to be written (future await).context(_)?, then the flow would always be clear.

(Personally, I see no benefit of “expr space await" over "await space expr” (as now you use parenthesis in all the same places), but this is a new point from this thread that potentially removes a problem of "space await".)

Also, a strong vocal support of doing await expr now and introducing .await later as part of a more general .keyword rather than ad-hocing it for await first. (Though that argument can be made either way esp. in regards to incrementallity.)

2 Likes

As I said, I was merely trying to aggregate arguments that I thought might have originated in this thread, without stating whether I agree or disagree. I must admit I don't really follow the "direction of eye movement" argument, so your comment would probably be better as a reply to @BrianMWest's original comment.

I agree.

With respect to accessibility, if indeed there were evidence that .await made coding with a screen reader more difficult, I'd be inclined to take that seriously regardless of whether ? also makes coding with a screen reader more difficult. Really, I just wish we had someone with specific accessibility needs weighing in on this; as it is, we have many non-disabled programmers (including myself) weighing the pros and cons of the various proposals, apparently with very little knowledge of how to ensure accessibility.

In any case, I do think .await is different from ? in this regard, because ? does not change the "happy path" control flow (which is often[citation needed] considered the primary control flow that should be considered for readability), whereas that is explicitly the purpose of .await. Concretely, if ? changes the control flow, then the remainder of the function (for a specific execution) does not matter.

A specific case in which .await could cause a readability problem that would not be posed by ? would arise when combining async/await code with resource-contentious multithreaded code. A ? following the acquisition of a mutex-lock would terminate the critical section; would await release the lock, or not? Either way, understanding at a glance whether or not a critical section contains an await seems valuable. (I don't actually know how await interacts with Drop types, unfortunately.)

Hm, I suppose that was indeed pretty similar to BrianMWest's point. Sorry for missing the overlap.

3 Likes

Right, any syntax would require you to respect the order of the await calls.

The point I raised in my post was not that + should be commutative. Instead I said out that swapping two lines that look like they only access a field (the .await syntax) now change the behavior of the program.

This is of course just a different way of saying that the .await syntax (in my opinion) is too subtle since it overloads an existing and very different syntax.

I call field access very different because it doesn't have side effects — but .await does.

The main argument against postfix macro await is that, await can not be implemented as a macro. While this is true under the current constraint, it’s not true if we implement await as our ordinary prefix keyword. (You may wonder ‘Wait what? Are you proposing a prefix syntax or a postfix syntax?’ Don’t worry. I’m going to explain that.)

Let’s recall why we needed to discuss the await syntax in the first place. The main reason is that, we want to chain await with method calls and more importantly, the ? operator. This problem can be generalized as, we want a mechanism such that we can chain some operations with some other operations. Surprisingly, this is not the first time we encounter this problem. And I’m not sure if the await syntax will be the last time we encounter this problem. The operator at the heart of await syntax discussion, the ? operator, is a feature we introduced to let us chain try! macro with method calls. While I personally enjoyed using ?, there’s no doubt that it’s one of the controversial feature. (From what I observed in different forums) Mainly because it adds a special rule to the rust language syntax to solve an ergonomic problem. And now we are introducing another special case, postfix keyword, to solve yet another ergonomic problem. I would agree with the choice if we had no other options. But I feel postfix macro would solve this problem perfectly.

So what postfix macro can bring to us?

  1. await can be implemented as a postfix macro, if we have an await prefix keyword.
the_bright_future_of_rust?.await!()?.is_here()?;
// expands to
(await the_bright_future_of_rust?)?.is_here()?;
  1. The ? operator can be replaced by try!.
the_result.try!().foo();
  1. Postfix keyword is possible
the_enum.match!{
  Foo(e) => e,
  Bar(e) => return Err(e),
};
// expands to
match (the_enum) {
  Foo(e) => e,
  Bar(e) => return Err(e),
}
  1. Or even
the_result.try_handle!(e => unimplemented!() // handle my error);
// expands to
match (the_result) {
    Ok(e) => e,
    Err(e) => unimplemented!() // handle my error
};

x.operator!(+, y)
 .operator!(*, z);
// exapands to
((x + y) * z)

You probably have noticed that, postfix macro is not just solving ‘chaining methods with result’ or chaining methods methods with await, but solving the root problem we have: ‘chaining some operations with some other oprations’. That’s why I find postfix macro is a real appealing solution.

In terms of implementation plan of await, we could take one of the following options

  1. Start with a prefix keyword and work towards postfix generic macro.
  2. Start with a postfix await special macro and work towards postfix generic macro, after we have postfix generic macro, convert the await macro to a normal macro with a keyword. With option 1, even if we don’t end up having generic postfix macro, we can still work towards posfix keyword as proposed in boat’s post. With option 2, even if we don’t end up having generic postfix macro, most users still wouldn’t have to know that await is not implementable as a macro.

TLDR: postfix macro solves the root problem of chaining operations. We should implement await using posfix macro.

link to original reddit post https://www.reddit.com/r/rust/comments/bmhmtw/what_postfix_macro_could_bring_to_rust_asyncawait/

6 Likes

You’ve overlooked that this also requires somehow making the keyword work as a macro. Not that it’s impossible, but it’s another thing that has to be done to make a postfix solution work, as all of await, try, match are keywords.

The corresponding postfix macro doesn’t need to have the same name though. They could be, await_for!, do_if, etc. or we could a macro, postfix!(keyword, …) to handle them all. The details definitely still need to be discussed. But the key point is still there, postfix macro solves the fundamental problem of chaining operations.

Sorry for the format, I’m on mobile.

Best regards, Weiwei Lin

I agree, though would nuance it a bit more to note that it's not just await, but await along with ?. Given that most futures are fallible -- to the extent that the original Future trait was only fallible -- the choice is not between await foo and foo.await but between some sort of await foo? and foo.await?. It's possible that, even if prefix await were better for screenreaders in C# with exceptions, .await? is easier than having them separate.

I think both sides can be argued reasonably here. One can also say that the goal in both cases is to make the code look as similar as possible to the infallible or synchronous code, only looking at the rethrow or interruption points later if for some reason you care (which you often don't), and thus being less in-your-face is an advantage, as it is for ?.

5 Likes

I strongly dislike postfix keyword syntax. Here’s why:

Readability: We can only say we “await something”

When we wait for a future, we say we await future, not future.await.

We say “future awaits” (future.await) when we mean a future is approaching, which somewhat makes sense in some other context but does not describe what we are doing.

What postfix await is trying to solve is but one niche use case

The only reason (that I’ve seen so far) to have a postfix syntax is method chaining and error handling:

let _ = future.await?.something_else();

But this is but one niche use case, is it really worth sacrificing readability and familiarity of classic prefix await syntax?

I strongly dislike postfix keywords (in general) as well

I mean, just look at this abomination:

codition.if { foo }.then { bar }

Alternate proposal 1: Postfix Symbol

I only dislike postfix keywords, but I am OK with postfix symbol, especially since I love using postfix ? syntax.

The above code snippet can be rewritten into this:

let _ = future@?.something_else();

Not only is this syntax way shorter than postfix await, it is also consistent with the rest of the language, especially postfix ? syntax.

Alternate proposal 2: Smart Pipeline

Dot syntax was never designed for method chaining, it just so happen that it is possible and JavaScript popularized it.

But pipeline operator was born for that purpose.

With smart pipeline, the above code snippet can be rewritten into this:

let _ = future |> await _ |> _? |> .something_else();

Or this:

let _ = future |> (await _)? |> .something_else();

Or this:

let _ = future |> await? |> .something_else();
3 Likes

We also say we "write to the file" and "lock the mutex", but in Rust that's file.write() and mutex.lock().

22 Likes

Further to @comex's point, you might say "Flatten an iterator" but we write iter.flatten().

If we have:

get_client_sme(wlan_svc, iface_id).await?
    .disconnect().await
    .map_err(|e| format_err!("error sending disconnect request: {}", e))

I would read this as:

Get the sme client with the wlan_svc and the iface_id, await it and re-throw, then disconnect and await, and map the error.

I don't think we are sacrificing readability. @scottmcm has made fine points about how .await and ? offers good readability and works well with left-to-right (data-flow order) reading flow.

We also disagree that it is niche. As seen in the code snippet oft cited in this thread, await is followed by . 20 times out of 43, which is 46.5% of the time.

Citation needed?

10 Likes