rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
98.96k stars 12.79k forks source link

Resolve `await` syntax #57640

Closed cramertj closed 5 years ago

cramertj commented 5 years ago

Before commenting in this thread, please check https://github.com/rust-lang/rust/issues/50547 and try to check that you're not duplicating arguments that have already been made there.


Notes from shepherds:

If you're new to this thread, consider starting from https://github.com/rust-lang/rust/issues/57640#issuecomment-456617889, which was followed by three great summary comments, the latest of which were https://github.com/rust-lang/rust/issues/57640#issuecomment-457101180. (Thanks, @traviscross!)

I60R commented 5 years ago

Another option could be using become keyword instead of await:

async fn become_chain() -> Result<usize, Error> {
    let _ = partial_computation_future(become)
        .unwrap_or_else(or_recover_future)
        .start(become)?;
}

client.get("https://my_api")
    .send_future(become)?
    .json_future(become)?

let output = future.start(become);
L-as commented 5 years ago

become could be a keyword for guaranteed TCO though

spacekookie commented 5 years ago

Thanks @EyeOfPython for that overview. That's especially useful for people just now joining the bike-shed.

Personally I'd hope we stay away from f await, just because it's very un-rustic syntax wise and makes it seem kinda special and magic. It would become one of these things that users new to Rust are going to be confused about a lot and I feel it doesn't add that much clarity, even to Rust veterans, to be worth that.

jplatte commented 5 years ago

@novacrazy the problem with this argument is that any await! macro would be magic, it's performing an operation that is not possible in user-written code

@Nemo157 I agree that an await! macro would be magic, but I would argue that this is not a problem. There are multiple such macros in std already, e.g. compile_error! and I haven't seen anyone complain about them. I think it's normal to use a macro only understanding what it does, not how it does it.

I agree with previous commenters that postfix would be the most ergonomic, but I would rather start out with prefix-macro await!(expr) and potentially transition to postfix-macro once that is a thing rather than have expr.await (magic compiler-builtin field) or expr.await() (magic compiler-builtin method). Both of those would introduce a completely new syntax purely for this feature, and IMO that just makes the language feel inconsistent.

HeroicKatora commented 5 years ago

@EyeOfPython Mind adding future(await) to your lists and table? All the positives of your assessment of future.await() seem to transfer without the weakness

Centril commented 5 years ago

Since some have argued that despite syntax highlighting, foo.await looks too much like a field access, we could change the token . to # and instead write foo#await. For example:

let foo = alpha()#await?
    .beta#await
    .some_other_stuff()#await?
    .even_more_stuff()#await
    .stuff_and_stuff();

To illustrate how GitHub would render this with syntax highlighting, let's replace await with match since they are of equal length:

let foo = alpha()#match?
    .beta#match
    .some_other_stuff()#match?
    .even_more_stuff()#match
    .stuff_and_stuff();

This seems both clear and ergonomic.

The rationale for # rather some other token is not specific, but the token is quite visible which helps.

I60R commented 5 years ago

So, another concept: if future was reference:

async fn log_service(&self) -> T {
   let service = self.myService.foo(); // Only construction
   *self.logger.log("beginning service call");
   let output = *service.exec(); // Actually wait for its result
   *self.logger.log("foo executed with result {}.", output);
   output
}

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = *acquire_lock();
    // Very terse, construct the future, wait on it, branch on its result.
    let length = (*logger.log_into(message))?;
    *logger.timestamp();
    Ok(length)
}

async fn await_chain() -> Result<usize, Error> {
    *(*partial_computation()).unwrap_or_else(or_recover);
}

(*(*client.get("https://my_api").send())?.json())?

let output = *future;

This would be really ugly and inconsistent. Let at least disambiguate that syntax:

async fn log_service(&self) -> T {
   let service = self.myService.foo(); // Only construction
   $self.logger.log("beginning service call");
   let output = $service.exec(); // Actually wait for its result
   $self.logger.log("foo executed with result {}.", output);
   output
}

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = $acquire_lock();
    // Very terse, construct the future, wait on it, branch on its result.
    let length = ($logger.log_into(message))?;
    $logger.timestamp();
    Ok(length)
}

async fn await_chain() -> Result<usize, Error> {
    $($partial_computation()).unwrap_or_else(or_recover);
}

($($client.get("https://my_api").send())?.json())?

let output = $future;

Better, but still ugly (and yet worse it makes github's syntax highlighting). However, to deal with that let introduce ability to delay prefix operator:

async fn log_service(&self) -> T {
   let service = self.myService.foo(); // Only construction
   self.logger.$log("beginning service call");
   let output = service.$exec(); // Actually wait for its result
   self.logger.$log("foo executed with result {}.", output);
   output
}

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = $acquire_lock();
    // Very terse, construct the future, wait on it, branch on its result.
    let length = logger.$log_into(message)?;
    logger.$timestamp();
    Ok(length)
}

async fn await_chain() -> Result<usize, Error> {
    ($partial_computation()).$unwrap_or_else(or_recover);
}

client.get("https://my_api").$send()?.$json()?

let output = $future;

This exactly what I want! Not only for await ($) but as well for deref (*) and negate (!).

Some caveats in this syntax:

  1. Operator precedence: for me it's obvious but for other users?
  2. Macros interop: would $ symbol cause problems here?
  3. Expression inconsistency: leading prefix operator applies to the whole expression while delayed prefix operator applies only to . delimited expression (await_chain fn demonstrate that), is that confusing?
  4. Parsing and implementation: is that syntax valid at all?
matprec commented 5 years ago

@Centril IMO # is too close to the raw literal syntax r#"A String with "quotes""#

Centril commented 5 years ago

IMO # is too close to the raw literal syntax r#"A String with "quotes""#

It seems quite clear from the context what the difference is in this case.

L-as commented 5 years ago

@Centril It does, but the syntax is also really foreign to the style Rust uses IMHO. It does not resemble any existing syntax with a similar function.

Centril commented 5 years ago

@Laaas Neither did ? when it was introduced. I would be happy to go with .await; but others seem unhappy with that so I am trying to find something else that works (i.e. is clear, ergonomic, sufficiently easy to type, chainable, has good precedence) and foo#await seems to satisfy all of that.

L-as commented 5 years ago

? has a bunch of other good points though, while #await seems rather arbitrary. In any case, if you want something like .await, why not .await!?

HeroicKatora commented 5 years ago

@Centril That was the main rationale behind future(await), to avoid the field access while not having to add any extra operators or foreign syntax.

Centril commented 5 years ago

while #await seems rather arbitrary.

# is arbitrary, yes -- is that make or break? When new syntax is invented at some point it has to be arbitrary because someone thought it looked good or made sense.

why not .await!?

That's equally arbitrary but I have no opposition to it.

@Centril That was the main rationale behind future(await), to avoid the field access while not having to add any extra operators or foreign syntax.

Instead it looks like function application where you are passing await to future; that strikes me as more confusing than "field access".

HeroicKatora commented 5 years ago

Instead it looks like function application where you are passing await to future; that strikes me as more confusing than "field access".

To me that was part of the conciseness. Await is in many aspects like a function call (callee executes between you and your result), and passing a keyword to that function made it clear that this is a different sort of call. But I see how it could be confusing that the keyword is similar to any other argument. (Minus syntax highlighting and compiler error messages trying to reinforce that message. Futures can not be called any other way than being awaited and vice versa).

L-as commented 5 years ago

@Centril

Being arbitrary isn't a negative thing, but having resemblance to an existing syntax is a positive thing. .await! isn't as arbitrary, since it's not entirely new syntax; after all, it's just a post-fix macro named await.

jplatte commented 5 years ago

To clarify / add to the point about new syntax specifically for await, what I mean by that is introducing new syntax that's unlike the existing syntax. There is a lot of prefix keywords, some of which have been added after Rust 1.0 (e.g. union), but AFAIK not a single postfix keyword (no matter if separated by a space, a dot, or something else). I also can't think of any other language that has postfix keywords.

IMHO the only postfix syntax that doesn't significantly increase Rust's strangeness is the postfix macro, becauses it mirrors method calls but can clearly be identified as a macro by anyone who has seen a Rust macro before.

swfsql commented 5 years ago

I also liked the standard await!() macro. It's just clear and simple. I prefer a field-like access (or any other postfix) to co-exist as awaited.

And I also liked the @? as ?-like sugar. This is personal, but I actually prefer the &? combination, because @ tends to be too tall and undershoot the line, which is very distractible. & is also tall (good) but doesn't undershoots (also good). Unfortunately & already has a meaning, although it would always be followed by an ?.. I guess?

Eg.

Lorem@?
  .ipsum@?
  .dolor()@?
  .sit()@?
  .amet@?
Lorem@?.ipsum@?.dolor()@?.sit()@?.amet@?

Lorem&?
  .ipsum&?
  .dolor()&?
  .sit()&?
  .amet&?
Lorem&?.ipsum&?.dolor()&?.sit()&?.amet&?

For me, the @ feels like a bloated character, distracts the reading flow. On the other hand, &? feels pleasant, doesn't distracts (my) reading and has a nice top space between the & and the ?.

L-as commented 5 years ago

I personally think a simple await! macro should be used. If post-fix macros get into the language, then the macro could simply be expanded, no?

Centril commented 5 years ago

@Laaas As much as I would like for there to be, there are no postfix macros in Rust yet, so they are new syntax. Also note that foo.await!.bar and foo.await!().bar aren't the same surface syntax. In the latter case there would be an actual postfix and builtin macro (which entails giving up on await as a keyword).

@jplatte

There is a lot of prefix keywords, some of which have been added after Rust 1.0 (e.g. union),

union is not a unary expression operator so it is irrelevant in this comparison. There are exactly 3 stable unary prefix operators in Rust (return, break, and continue) and all of them are typed at !.

I60R commented 5 years ago

Hmm... With @ my previous proposal looks slightly better:

client.get("https://my_api").@send()?.@json()?

let output = @future;

let foo = (@alpha())?
    .@beta
    .@some_other_stuff()?
    .@even_more_stuff()
    .stuff_and_stuff();
L-as commented 5 years ago

@Centril That is my point, since we do not have post-fix macros yet, it should simply be await!(). Also I meant .await!() FYI. I do not think await needs to be a keyword, though you could reserve it if you find it problematic.

jplatte commented 5 years ago

union is not a unary expression operator so it is irrelevant in this comparison. There are exactly 3 stable unary prefix operators in Rust (return, break, and continue) and all of them are typed at !.

This still means that the prefix-keyword variant fits into the group of unary expression operators, even if typed differently. The postfix-keyword variant is a much larger divergence from existing syntax, one that is too large in relation to await's importance in the language in my opinion.

HeroicKatora commented 5 years ago

Regarding prefix macro and possibility of transitioning to post-fix: await needs to remain a keyword for this to work. The macro would then be some special lang item where it's allowed to use a keyword as the name without an extra r#, and r#await!() could but probably should not invoke the same macro. Other than that it seems like the most pragmatic solution to make it available.

That is, keep await as a keyword but make await! resolve to a lang-item macro.

L-as commented 5 years ago

@HeroicKatora why does it need to remain a keyword for it to work?

HeroicKatora commented 5 years ago

@Laaas Because if we want to keep the possibility of transitioning open, we need to remain open for future post-fix syntax that uses it as a keyword. Therefore, we need to keep the keyword reservation so that we don't need an edition break for the transition.

HeroicKatora commented 5 years ago

@Centril

Also note that foo.await!.bar and foo.await!().bar aren't the same surface syntax. In the latter case there would be an actual postfix and builtin macro (which entails giving up on await as a keyword).

Could this not be solved by making the keyword await combined with ! resolve to an internal macro (which can not be defined through normal means)? Then it remains a keyword but resolves to a macro in otherwise macro syntax.

L-as commented 5 years ago

@HeroicKatora Why would await in x.await!() be a reserved keyword?

HeroicKatora commented 5 years ago

It would not be, but if we keep post-fix unresolved, it need not be the solution we arrive at in later discussions. If that were the unique agreed upon best possibility, then we should adopt this exact post-fix syntax in the first place.

DDOtten commented 5 years ago

This is another time that we come across something that works a lot better as a postfix operator. The big example of this is try! which we eventually gave its own symbol ?. However I think this is not the last time where a postfix operator is more optimal and we can not give everything its own special charachter. So I think we should at least not start with @. It would be a lot better if we would have a way to do this kinds of things. That is why I support the postfix macro style .await!().

let x = foo().try!();
let y = bar().await!();

But for this to make sense postfix macros them self would have to be introduced. Therefore I think it would be best to start with a normal await!(foo) macro syntax. We could later expand this to either foo.await!() or even foo@ if we really feel this is important enough to warrant its own symbol. That this macro would need a bit of magic is not new to std and to me is not a big problem. As @jplatte put it:

@Nemo157 I agree that an await! macro would be magic, but I would argue that this is not a problem. There are multiple such macros in std already, e.g. compile_error! and I haven't seen anyone complain about them. I think it's normal to use a macro only understanding what it does, not how it does it.

Keruspe commented 5 years ago

A recurring issue I see discussed here is about chaining and how to use await in chaining expressions. Maybe there could be another solution ?

If we want not to use await for chaining and only use await for assignments, we could have something like just replacing let with await : await foo = future

Then for chaining we could imagine some kind of operation like await res = fut1 -> fut2 or await res = fut1 >>= fut2.

The only case missing is waiting and returning the result, some shortcut for await res = fut; res. This could be easily done with a plain await fut

I don’t think I’ve seen another proposal like this one (at least for the chaining), so dropping that here as I think it would be nice to use.

EyeOfPython commented 5 years ago

@HeroicKatora I've added fut(await) to the list and ranked it according to my opinion.

If anyone feels like my scoring is off, please tell me!

arielb1 commented 5 years ago

Syntax-wise, I think .await!() is clean and allows for chaining while not adding any oddities to the syntax.

However, if we ever get "real" postfix macros, it will be a little weird, because presumably .r#await!() could be shadowed, while .await!() couldn't.

nicoburns commented 5 years ago

I'm pretty strongly against the "postfix keyword" option (separated with a space like: foo() await?.bar() await?), because I find the fact that the await is joined to the following part of the expression, and not the part that it operates on. I would prefer pretty much any symbol other than whitespace here, and would even prefer prefix syntax over this despite it's disadvantages with long chains.

I think "obvious precedence" (with the await? sugar) is clearly the best prefix option, as mandatory delimiters is a pain for the very common case of awaiting a single statement, and "useful precedence" is not at all intuitive, and thus confusing.

DDOtten commented 5 years ago

Syntax-wise, I think .await!() is clean and allows for chaining while not adding any oddities to the syntax.

However, if we ever get "real" postfix macros, it will be a little weird, because presumably .r#await!() could be shadowed, while .await!() couldn't.

If we get postfix macros and we use .await!() then we can unreserve await as a keyword and just make it a postfix macro. The implementation of this macro would still require a bit of magic of cource but it would be a real macro just like compiler_error! is today.

HeroicKatora commented 5 years ago

@EyeOfPython Could you maybe explain in detail which changes you consider in forward-compatibility? I'm unsure in which way fut@await would rate higher than fut.await!() and await { future } lower than await!(future). The verbosity column also seems a bit strange, some expressions are short but have lower rating, does it consider chained statements, etc. Everything else seems balanced and like the most well condensed extensive assessment so far.

ivandardi commented 5 years ago

After reading this discussion, it seems that lots of people want to add a normal await!() macro and figure out the postfix version later. That is assuming we actually want a postfix version, which I'll take as true for the rest of this comment.

So I'd just like to poll the opinion of everyone here: Should we all agree on the await!(future) syntax FOR NOW? The pros are that there's no parsing ambiguity with that syntax, and it's also a macro, so no changing the language's syntax to support this change. The cons are that it will look ugly for chaining, but that doesn't matter since this syntax can be easily replaced with a postfix version automatically.

Once we settle that down and get it implemented into the language, we can then continue the discussion for postfix await syntax with hopefully more mature experiences, ideas and possibly other compiler features.

EyeOfPython commented 5 years ago

@HeroicKatora I rated it higher as it could free await to be used as ordinary identifier naturally and it would allow other postfix operators to be added, whereas I think fut.await!() would better be if await was reserved. However, I'm unsure if this is reasonable, it also seems definitely valid that fut.await!() could be higher.

For await { future } vs await!(future), the latter one keeps the option open to change to almost any of the other options, whereas the first one only really allows either of the await future variants (as laid out in @withoutboats' blog entry). So I think it should definitely be fwd(await { future }) < fwd(await!(future)).

As for the verbosity, you are correct, after splitting cases into subgroups I didn't re-evaluate the verbosity, which should be the most objective one of all.

I'll edit it to take your comments into account, thank you!

Centril commented 5 years ago

Stabilizing await!(future) is about the worst possible option I can imagine:

  1. It means we have to unreserve await which further means that future language design gets harder.
  2. It is knowingly taking the same route as with try!(result) which we deprecated (and which requires writing r#try!(result) in Rust 2018).
    • If we know that await!(future) is bad syntax we eventually mean to deprecate, this is willfully creating technical debt.
    • Moreover, try!(..) is defined in Rust whereas await!(future) cannot be and would instead be compiler magic.
    • Deprecating try!(..) was not easy and took a toll socially. Going through that ordeal again doesn't seem appealing.
  3. This would use macro syntax for one a central and important part of the language; that seems distinctly not first-class.
  4. await!(future) is noisy; unlike await future you need to write !( ... ).
  5. Rust APIs, and especially the standard library, are centered around method call syntax. For example, it is commonplace to chain methods when dealing with Iterators. Similar to await { future } and await future, the await!(future) syntax will make method chaining difficult and induce temporary let bindings. This is bad for ergonomics and in my view for readability.
HeroicKatora commented 5 years ago

@Centril I'd like to agree but there are some open questions. Are you sure about 1.? If we make it 'magic' could we not make it even more magic by allowing this to refer to a macro without dropping it as a keyword?

I joined Rust too late to evaluate the social perspective of 2.. Repeating a mistake doesn't sound appealing but as some code still was not converted from try! it should be evident that it was a solution. That brings up the question of whether we intend to have async/await as an incentive to migrate to edition 2018 or rather be patient and not repeat this.

Two other (imho) central features very much like 3.: vec![] and format!/println!. The former very much because there is no stable boxed construction afaik, the latter due to format string construction and not having dependently typed expressions. I think these comparisons also partially put 4. into another perspective.

ejmahler commented 5 years ago

I oppose any syntax that doesn't read somewhat like english. IE, "await x" reads something like english. "x#!!@!&" does not. "x.await" reads tantalizingly like english, but it won't when x is a non-trivial line, like a member function call with long names, or a bunch of chained iterator methods, etc.

More specifically, I support "keyword x", where keyword is probably await. I come from using both the C++ coroutines TS and unity's c# coroutines, which both use a syntax very similar to that. And after years of using them in production code, my belief is that knowing where your yield points are, at a glance, is absolutely critical. When you skim down the indent line if your function, you can pick out every co_await/yield return in a 200-line function in a matter of seconds, with no cognitive load.

The same is not true of the dot operator with await afterwards, or some other postfix "pile of symbols" syntax.

I believe that await is a fundamental a control flow operation. It should be given the same level of respect as 'if, while, match, and return. Imagine if any of those were postfix operators - reading Rust code would be a nightmare. Like with my argument for await, as it is you can skim the indent line of any Rust function and immediately pick out all of the control flow. There are exceptions, but they're exceptions, and not something we should strive for.

andreytkachenko commented 5 years ago

I am agree with @ejmahler. We should not forget of other side of development - code review. File with source code is much more often being read then wrote, hence I thing it should be easier to read-and-understand then to write. Finding the yielding points is really important on code review. And I personally would vote for Useful precedence. I believe that this:

...
let response = await client.get("https://my_api").send()?;
let body: MyResponse = await response.into_json()?;

is easier for understanding rather then this:

...
let body: MyResponse = client.get("https://my_api").send().await?.into_json().await?;
Centril commented 5 years ago

@HeroicKatora

@Centril I'd like to agree but there are some open questions. Are you sure about 1.? If we make it 'magic' could we not make it even more magic by allowing this to refer to a macro without dropping it as a keyword?

Technically? Possibly. However, there should be strong justification for special cases and in this case having magic upon magic doesn't seem justified.

That brings up the question of whether we intend to have async/await as an incentive to migrate to edition 2018 or rather be patient and not repeat this.

I don't know if we ever said we intended for new module system, async/await, and try { .. } to be incentives; but irrespective of our intent they are, and I think that's a good thing. We want people to eventually start using new language features to write better and more idiomatic libraries.

Two other (imho) central features very much like 3.: vec![] and format!/println!. The former very much because there is no stable boxed construction afaik,

The former exists, and is written vec![1, 2, 3, ..], to mimic array literal expressions, e.g. [1, 2, 3, ..].

@ejmahler

"x.await" reads tantalizingly like english, but it won't when x is a non-trivial line, like a member function call with long names, or a bunch of chained iterator methods, etc.

What's wrong with a bunch of chained iterator methods? That's distinctly idiomatic Rust. The rustfmt tool will also format method chains on different lines so you get (again using match to show the syntax highlighting):

let foo = alpha().match?  // or `alpha() match?`, `alpha()#match?`, `alpha().match!()?`
    .beta
    .some_other_stuff().match?
    .even_more_stuff().match
    .stuff_and_stuff();

If you read .await as "then await" it reads perfectly, at least to me.

And after years of using them in production code, my belief is that knowing where your yield points are, at a glance, is absolutely critical.

I don't see how postfix await negates that, especially in the rustfmt formatting above. Moreover, you can write:

let foo = alpha().match?;
let bar = foo.beta.some_other_stuff().match?;
let baz = bar..even_more_stuff().match;
let quux = baz.stuff_and_stuff();

if you fancy that.

in a 200-line function in a matter of seconds, with no cognitive load.

Without knowing too much about the particular function, it seems to me that 200 LOC probably violates the single responsibility principle and does too much. The solution is to make it do less and split it up. In fact, I think that is the most important thing for maintainability and readability.

I believe that await is a fundamental a control flow operation.

So is ?. In fact, await and ? are both effectful control flow operations that say "extract value out of context". In other words, in the local context, you can imagine these operators having the type await : impl Future<Output = T> -> T and ? : impl Try<Ok = T> -> T.

There are exceptions, but they're exceptions, and not something we should strive for.

And the exception here is ? ?

@andreytkachenko

I am agree with @ejmahler. We should not forget of other side of development - code review. File with source code is much more often being read then wrote, hence I thing it should be easier to read-and-understand then to write.

The disagreement is around what would be best for readability and ergonomics.

is easier for understanding rather then this:

...
let body: MyResponse = client.get("https://my_api").send().await?.into_json().await?;

This is not how it would be formatted; running it through rustfmt gets you:

let body: MyResponse = client
    .get("https://my_api")
    .send()
    .match?
    .into_json()
    .match?;
HeroicKatora commented 5 years ago

@ejmahler @andreytkachenko I agree with @Centril here, the biggest change (some may say improvement, I would not) you gain from prefix syntax is that users are incentivized to split their statements over multiple lines because everything else is unreadable. That is not Rust-y and usual formatting rules make up for this in post-fix syntax. I also consider the yield-point to be more obscure in prefix syntax because await is not actually placed at the code point where you yield, rather opposed to it.

If you go this way, let's experiment for the sake of spelling it out, rather with await as a replacement for let in the spirit of @Keruspe 's idea to really enforce this. Without the other syntax extensions because they seem like a stretch.

await? response = client.get("https://my_api").send();
await? body: MyResponse = response.into_json();

But for none of these I see enough benefit to explain their composability loss and complications in the grammar.

ivandardi commented 5 years ago

Hm... is it desirable to have both a prefix and suffix form of await? Or just the suffix form?

ejmahler commented 5 years ago

Chaining method calls together is idiomatic specifically when talking about iterators and to a much lesser extent options, but aside from that, rust is an imperative language first and a functional language second.

I’m not arguing that a postfix is literally incomprehensible, I’m makin a cognitive load argument that hiding a top-tier control flow operation, carrying the same importance as ‘return’, increases cognitive load when compared to putting it as close to the beginning of the line - and I’m making this argument based on years of production experience.

On Sat, Jan 19, 2019 at 11:59 AM Mazdak Farrokhzad notifications@github.com wrote:

@HeroicKatora https://github.com/HeroicKatora

@Centril https://github.com/Centril I'd like to agree but there are some open questions. Are you sure about 1.? If we make it 'magic' could we not make it even more magic by allowing this to refer to a macro without dropping it as a keyword?

Technically? Possibly. However, there should be strong justification for special cases and in this case having magic upon magic doesn't seem justified.

That brings up the question of whether we intend to have async/await as an incentive to migrate to edition 2018 or rather be patient and not repeat this.

I don't know if we ever said we intended for new module system, async/ await, and try { .. } to be incentives; but irrespective of our intent they are, and I think that's a good thing. We want people to eventually start using new language features to write better and more idiomatic libraries.

Two other (imho) central features very much like 3.: vec![] and format!/ println!. The former very much because there is no stable boxed construction afaik,

The former exists, and is written vec![1, 2, 3, ..], to mimic array literal expressions, e.g. [1, 2, 3, ..].

@ejmahler https://github.com/ejmahler

"x.await" reads tantalizingly like english, but it won't when x is a non-trivial line, like a member function call with long names, or a bunch of chained iterator methods, etc.

What's wrong with a bunch of chained iterator methods? That's distinctly idiomatic Rust. The rustfmt tool will also format method chains on different lines so you get (again using match to show the syntax highlighting):

let foo = alpha().match? // or alpha() match?, alpha()#match?, alpha().match!()? .beta .some_other_stuff().match? .even_more_stuff().match .stuff_and_stuff();

If you read .await as "then await" it reads perfectly, at least to me.

And after years of using them in production code, my belief is that knowing where your yield points are, at a glance, is absolutely critical.

I don't see how postfix await negates that, especially in the rustfmt formatting above. Moreover, you can write:

let foo = alpha().match?;let bar = foo.beta.some_other_stuff().match?;let baz = bar..even_more_stuff().match;let quux = baz.stuff_and_stuff();

if you fancy that.

in a 200-line function in a matter of seconds, with no cognitive load.

Without knowing too much about the particular function, it seems to me that 200 LOC probably violates the single responsibility principle and does too much. The solution is to make it do less and split it up. In fact, I think that is the most important thing for maintainability and readability.

I believe that await is a fundamental a control flow operation.

So is ?. In fact, await and ? are both effectful control flow operations that say "extract value out of context". In other words, in the local context, you can imagine these operators having the type await : impl Future -> T and ? : impl Try -> T.

There are exceptions, but they're exceptions, and not something we should strive for.

And the exception here is ? ?

@andreytkachenko https://github.com/andreytkachenko

I am agree with @ejmahler https://github.com/ejmahler. We should not forget of other side of development - code review. File with source code is much more often being read then wrote, hence I thing it should be easier to read-and-understand then to write.

The disagreement is around what would be best for readability and ergonomics.

is easier for understanding rather then this:

...let body: MyResponse = client.get("https://my_api").send().await?.into_json().await?;

This is not how it would be formatted; the idiomatic rustfmt formatting is:

let body: MyResponse = client .get("https://my_api") .send() .match? .into_json() .match?;

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/rust-lang/rust/issues/57640#issuecomment-455810497, or mute the thread https://github.com/notifications/unsubscribe-auth/ABGmeocFJpPKaypvQHo9LpAniGOUFrmzks5vE3kXgaJpZM4aBlba .

Centril commented 5 years ago

@ejmahler We disagree re. "hiding"; the same arguments were made wrt. the ? operator:

The ? operator is very short, and has been criticised in the past to "hide" a return. Most times code is read, not written. Renaming it to ?! will make it two times as long and therefore harder to overlook.

Nevertheless, we ultimately stabilized ? and since then I think the prophecy has failed to materialize. To me, postfix await follows natural reading order (at least for left-to-right language speakers). In particular, it follows data-flow order.

ivandardi commented 5 years ago

Not to mention syntax highlighting: anything await-related can be highlighted with a bright color, so they can be found at a glance. So even if we had a symbol instead of the actual await word, it would still be very readable and findable in syntax highlighted code. With that said, I still prefer the use of the await word just for grepping reasons - it's easier to grep the code for anything that's being awaited if we only use the await word instead of a symbol like @ or #, whose meaning is grammar dependent.

ben0x539 commented 5 years ago

y'all this isn't rocket science

let body: MyResponse = client.get("https://my_api").send()...?.into_json()...?;

postfix ... is extremely readable, hard to miss at a glance and super intuitive since you naturally read it as the code kind of trailing off while it waits for the result of the future to become available. no precedence/macro shenanigans necessary and no extra line noise from unfamiliar sigils, since everybody has seen ellipses before.

(apologies to @solson)

HeroicKatora commented 5 years ago

@ben0x539 Does that mean I can access a member of my result like future()....start? Or await a range result like so range().....? And how exactly to you mean no precedence/macro shenanigans necessary and since currently ellipsis .. requires being a binary operator or paranthesis on the right and this is awfully close at a glance.