About the `await!` macro and possible `await` keyword addition


#1

There was a bit of a discussion on Reddit about the await! macro itself. I’m gonna relay the points here in case the Reddit thread goes boom.

The idea is to get rid of the await! macro/possible future keyword and use the ? operator. It essentially relies on the fact that a Future is a monad. We can compare it to an Option<T>, which is also a monad, and with the Try trait will soon get operator ? support.

You can use monadic bind with Option<T> via the .and_then method:

let this_is_an_option = Some(5);
let answer = this_is_an_option
    .and_then(|x| if x % 2 == 0 { Some(2 * x) } else { None });
return answer;

However, this can be rewritten with the ? operator like so:

let this_is_an_option = Some(5);
let answer = this_is_an_option?;
return if answer  % 2 == 0 { Some(2 * answer) } else { None });

Turns out that Futures also have a monadic bind called .and_then:

let this_is_a_future = client.get("https://www.rust-lang.org");
let answer = this_is_a_future
    .and_then(|x| if x.is_valid() { Some(x.body) } else { None });
return answer;

So instead of writing this with the current await! macro:

let this_is_a_future = client.get("https://www.rust-lang.org");
let answer = await!(this_is_a_future)?;
return if answer.is_valid() { Some(answer.body) } else { None };

We could try to extend the await functionality into the ? operator, like so:

let this_is_a_future = client.get("https://www.rust-lang.org");
let answer = this_is_a_future?;
return if answer.is_valid() { Some(answer.body) } else { None };

There are some questions about that, however. For example, the coroutine returns a Result, so things like ?? might become usual, which is a bit weird. There’s also the fact that Try as it currently is might not be able to include coroutine functions, so we would either have to change the Try trait to support them, or create a new Try trait specifically for async functions. I don’t know what the best course of action is here, because I’m not very knowledgeable on the internals right now, but that might be something worth looking at.


#2

Wouldn’t your last example be:

let this_is_a_future = client.get("https://www.rust-lang.org");
let answer = this_is_a_future??; // 2 ?
return if answer.is_valid() { Some(answer.body) } else { None };

If in the await example above ? is the equivalent to try!. My thoughts: the ?? looks really weird and it becomes hard to understand what’s going on in a function compared to the example with await. I would rather have another symbol than ? which I associate with early return.


#3

I don’t think I’d want to see try! and await!, which do entirely different things (error handling vs. concurrency), be conflated into a single syntactic sugar. I’m not too fond of the discoverability of ? to begin with. Hiding async blocking calls behind a now ambiguous sigil would make things even worse imho.


#4

While futures and Option and Result all have monadic structure, neither ? nor await! is a general do notation, they are both tailored to the semantics of Option/Result and futures respectively, and consequently they differ significantly from each other:

  • ? desguars to an early return
  • await turns the surrounding function into a state machine that returns and saves its state when encountering a “not ready” future, so it can resume in the same place when polled again.
  • Neither involves and_then, for a few reasons:
    • Simply wrapping the following lines into a closure would affect control flow in there (see pre-RFC: Allow return, continue, and break to affect the captured environment)
    • It’s not as simple as putting the following lines in a closure when the ?/await is, say, inside a loop. It would effectively require transforming the code into continuation passing style, or doing the aforementioned state machine transform. That’s more or less necessary with await, but for ? it’s unnecessary overhead.
    • Doing the state machine transform is simpler when you do it on MIR rather than trying to do CPS transforms on AST, it wouldn’t literally desugar to and_then.

#5

Agreed that try! and await! are very different things, and that overloading syntax is not a good idea.

It’s also a mild violation of Stroustrup’s rule – await! is new, and should not have terse syntax (yet).

Once it’s more widely deployed we can better assess the cost of overloading ?.


#6

Another reason why you might not want to collapse them is, that you might want to use both await to turn you function into an async state machine and ? to early return.

So you would have cases where ?? would be used to:

  • early return on a result and early return on the results result
  • await a item which is a result and early return errors
  • await a item which is a async state machine and await on it

And what if you implement some await enabling trait on a result? Should it then await or early return (through more likely you would not be able to implement it, due to conflicting implementations).

PS: I’m not completely up to date with all details of the current await impl. so I’m not sure if there is a trait for await analogous to the Try trait or if async-statemachines always returns a Result (they shouldn’t but maybe they do for some implementation reason). The point is even if non of this applies today, it might apply with potential extensions to await.