rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
99.06k stars 12.8k 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!)

ejmahler commented 5 years ago

Yes, the ? Operator exists. I already acknowledged that there were exceptions. But it’s an exception. The vast majority of control flow in any Rust program happens via prefix keywords.

On Sat, Jan 19, 2019 at 1:51 PM Benjamin Herr notifications@github.com wrote:

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.

— 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-455818177, or mute the thread https://github.com/notifications/unsubscribe-auth/ABGmen354fhk7snYsANTfp5oOuDb4OLYks5vE5NSgaJpZM4aBlba .

ben0x539 commented 5 years ago

@HeroicKatora That looks a bit artificial but yeah sure. I meant that since it's a postfix operation, like the other postfix solutions suggested, it avoids the need for counterintuitive precedence for await x?, and it's not a macro.

Centril commented 5 years ago

@ejmahler

Yes, the ? Operator exists. I already acknowledged that there were exceptions. But it’s an exception.

There are two expression forms that fit keyword expr, namely return expr and break expr. The former is more common than the latter. The continue 'label form doesn't really count since, while it is an expression, it isn't of form keyword expr. So now you have 2 whole prefix keyword unary expression forms and 1 postfix unary expression form. Before we even take into account that ? and await are more similar than await and return are, I'd hardly call return/break expr a rule for ? to be an exception against.

The vast majority of control flow in any Rust program happens via prefix keywords.

As aforementioned, break expr isn't all that common (break; is more typical and return; are more typical and they are not unary expression forms). What remains is early return expr;s and it doesn't seem at all clear to me that this is vastly more common than match, ?, just nested if lets and elses, and for loops. Once try { .. } stabilizes I'd expect ? to be used even more.

EyeOfPython commented 5 years ago

@ben0x539 I think we should reserve ... for variadic generics, once we're ready to have them

mitsuhiko commented 5 years ago

I really like the idea of innovating with postfix syntax here. It makes a lot more sense with the flow and I remember how much better code turned when we went from prefix try! to postfix ?. I think there is a lot of people that made the experience in the Rust community of how much improvements to code that made.

If we don't like the idea of .await I'm sure some creativity can be made to find an actual postfix operator. One example could just be to use ++ or @ for await.

yazaddaruvala commented 5 years ago

:( I just don’t want to await anymore.

Everyone is comfortable with macro syntax, most people in this thread that start with other opinions seem to end up favoring macro syntax.

Sure it’ll be a “magic macro” but users rarely care about what the macro expansion looks like and for those who do, it is easy enough to explain the nuance in the docs.

Regular macro syntax is kinda like apple pie, it is everyone’s second favorite option but as a result the family’s favorite option[0]. Importantly, like with try! we can always change it later. But most importantly, the sooner we can all agree the sooner we can all start actually using it and be productive!

[0] (Referenced in the first minute) https://www.ted.com/talks/kenneth_cukier_big_data_is_better_data/transcript?language=en

ejmahler commented 5 years ago

match, if, if let, while, while let, and for are all pervasive control flow that use prefixes. Pretending break and continue are the only control flow keywords is frustratingly misleading.

On Sat, Jan 19, 2019 at 3:37 PM Yazad Daruvala notifications@github.com wrote:

:( I just don’t want to await anymore.

Everyone is comfortable with macro syntax, most people in this thread that start with other opinions seem to end up favoring macro syntax.

Sure it’ll be a “magic macro” but users rarely care about what the macro expansion looks like and for those who do, it is easy enough to explain the nuance in the docs.

Regular macro syntax is kinda like apple pie, it is everyone’s second favorite option but as a result the family’s favorite option[0]. Importantly, like with try! we can always change it later. But most importantly, the sooner we can all agree the sooner we can all start actually using it and be productive!

[0] (Referenced in the first minute) https://www.ted.com/talks/kenneth_cukier_big_data_is_better_data/transcript?language=en

— 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-455824275, or mute the thread https://github.com/notifications/unsubscribe-auth/ABGmesz5_LfDKdcKn6zMO5uuSJs9lFiYks5vE6wygaJpZM4aBlba .

EyeOfPython commented 5 years ago

@mitsuhiko I agree! Postfix feels more rustic due to the chaining. I think the fut@await syntax I proposed is another interesting option that doesn't seem to have as many downsides as other proposals. I'm not sure though if it's too far out there and a more down-to-earth version would be preferable.

Centril commented 5 years ago

@ejmahler

match, if, if let, while, while let, and for are all pervasive control flow that use prefixes. Pretending break and continue are the only control flow keywords is frustratingly misleading.

It's not misleading at all. The relevant grammar for these constructs is roughly:

Expr = kind:ExprKind;
ExprKind =
  | If:{ "if" cond:Cond then:Block { "else" else_expr:ElseExpr }? };
  | Match:{ "match" expr:Expr "{" arms:MatchArm* "}" }
  | While:{ { label:LIFETIME ":" }? "while" cond:Cond body:Block }
  | For:{ { label:LIFETIME ":" }? "for" pat:Pat "in" expr:Expr body:Block }
  ;

Cond =
  | Bool:Expr
  | Let:{ "let" pat:Pat "=" expr:Expr }
  ;

ElseExpr =
  | Block:Block
  | If:If
  ;

MatchArm = pats:Pat+ % "|" { "if" guard:Expr }? "=>" body:Expr ","?;

Here, the forms are if/while expr block, for pat in expr block, and match expr { pat0 => expr0, .., patn => exprn }. There is a keyword that precedes what follows in all of these forms. I guess this is what you mean by "uses prefixes". However, these are all block forms and not unary prefix operators. The comparison with await expr is therefore misleading since there's no consistency or rule to speak of. If you are going for consistency with block forms, then compare that with await block, not await expr.

dpc commented 5 years ago

@mitsuhiko I agree! Postfix feels more rustic due to the chaining.

Rust is dualistic. It supports both imperative and functional approaches. And I think it's fine, because in different cases, each of them can be more suitable.

I don't know. Feels like it would be great to have both:

await foo.bar();
foo.bar().await;

Having used Scala for a while, I was quite fond of many things working like that too. Especially match and if would be a nice to have in postfix positions in Rust.

foo.bar().await.match {
   Bar1(x, y) => {x==y},
   Bar2(y) => {y==7},
}.if {
   bazinga();
}
CAD97 commented 5 years ago

Summary so far

Option matrices:

Summary matrix of options (using @ as the sigil, but could be mostly anything):

Name Future<T> Future<Result<T, E>> Result<Future<T>, E>
PREFIX - - -
Keyword Macro await!(fut) await!(fut)? await!(fut?)
Keyword Function await(fut) await(fut)? await(fut?)
Useful Precedence await fut await fut? await (fut?)
Obvious Precedence await fut await? fut await fut?
POSTFIX - - -
Fn With Keyword fut(await) fut(await)? fut?(await)
Keyword Field fut.await fut.await? fut?.await
Keyword Method fut.await() fut.await()? fut?.await()
Postfix Keyword Macro fut.await!() fut.await!()? fut?.await!()
Space Keyword fut await fut await? fut? await
Sigil Keyword fut@await fut@await? fut?@await
Sigil fut@ fut@? fut?@

"Sigil Keyword"'s sigil cannot be #, as then you couldn't do it with a future called r. ... as the sigil [would not have to change tokenization like my first worry](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&code=macro_rules!%20m%20%7B%0A%20%20%20%20(fut%20...%20.%20bar)%20%3D%3E%20%7B%7D%0A%7D%0A%0Afn%20main()%7B%0A%20%20%20%20m!(fut....bar)%3B%0A%7D%0A).

More real-life use (PM me other real use cases with multiple await on urlo and I'll add them):

Name (reqwest) Client \ > Client::get \ > RequestBuilder::send \ > await \ > ? \ > Response::json \ > ?
PREFIX -
Keyword Macro await!(client.get("url").send())?.json()?
Keyword Function await(client.get("url").send())?.json()?
Useful Precedence (await client.get("url").send()?).json()?
Obvious Precedence (await? client.get("url").send()).json()?
POSTFIX -
Fn With Keyword client.get("url").send()(await)?.json()?
Keyword Field client.get("url").send().await?.json()?
Keyword Method client.get("url").send().await()?.json()?
Postfix Keyword Macro client.get("url").send().await!()?.json()?
Space Keyword client.get("url").send() await?.json()?
Sigil Keyword client.get("url").send()@await?.json()?
Sigil client.get("url").send()@?.json()?

EDIT NOTE: It has been pointed out to me that it may make sense for Response::json to also return a Future, where send waits for the outgoing IO and json (or other interpretation of the result) waits for the incoming IO. I'm going to leave this example as-is, though, as I think it's meaningful to show that the chaining issue applies even with only one IO await point in the expression.

There seems to be rough consensus that of the prefix options, the obvious precedence (along with the await? sugar) is the most desirable. However, many people have spoken up in favor of a postfix solution, in order to make chaining as above easier. Although the prefix choice has rough consensus, there seems to be no consensus over what postfix solution is best. All of the proposed options lead to easy confusion (mitigated by keyword highlighting):

Other more drastic suggestions:

Stabilizing with a keyword macro await!(fut) is of course future-compatible with basically all of the above, though that does require making the macro use a keyword instead of a regular identifier.

If someone has an example mostly-real-ish example that uses two await in one chain, I'd love to see it; nobody's shared one so far. However, postfix await is also useful even if you don't need to await more than once in a chain, as shown in the reqwest example.

If I missed something notable above this summary comment, PM me on urlo and I'll try to add it in. (Though I will require it to be adding someone else's comments to avoid loud voice favoritism.)

CAD97 commented 5 years ago

Personally, I've historically been in favor of prefix keyword with the obvious precedence. I still think stabilizing with a keyword macro await!(fut) would be useful to gather real-world information about where the awaiting happens in real world use cases, and would still allow us to add a non-macro prefix or postfix option later.

However, in the process of writing the above summary, I started liking "Keyword Field". "Space Keyword" feels nice when split along multiple lines:

client
    .get("url")
    .send() await?
    .json()?

but on one line, it makes an awkward break that groups the expression poorly: client.get("url").send() await?.json()?. However, with the keyword field, it looks good in both forms: client.get("url").send().await?.json()?

client
    .get("url")
    .send()
    .await?
    .json()?

though I suppose a "keyword method" would flow better, as it is an action. We could even make it a "real" method on Future if we wanted:

trait Future<..> {
    ..
    extern "rust-await" fn r#await(self) -> _;
}

(extern "rust-await" would of course imply all of the magic required to actually do the await and it wouldn't actually be a real fn, it'd mainly just be there because the syntax looks like a method, if a keyword method is used.)

dpc commented 5 years ago

Allow both prefix (obvious precedence) and postfix "field" ...

If any postfix syntax is selected (no matter if together or instead of the prefix one), it would definitely be an argument for a future discussion: we now have keywords that work both in prefix and postfix notation, precisely because sometimes one is preferable over the other, so maybe we could just allow both where it makes sense, and increase the flexibility of the syntax, while unifying the rules. Maybe it's a bad idea, maybe it will be rejected, but it is definitely a discussion to be had in the future, if postix notation is used for await.

scottmcm commented 5 years ago

I think there's very little chance a syntax that does not include the character string await will be accepted for this syntax.

:+1:


A random thought I had after seeing a bunch of the examples here (such as @mehcode's): One of the complaints I remember about .await is that it's too hard to see†, but given that awaitable things are typically fallible, the fact that it's often .await? helps draw extra attention to it anyway.

† If you're using something that doesn't highlight keywords


@ejmahler

I oppose any syntax that doesn't read somewhat like english

Something like request.get().await reads just as well as something like body.lines().collect(). In "a bunch of chained iterator methods", I think prefix actually reads worse, since you have to remember that they said "wait" way back at the beginning, and never know when you hear something if it's going to be what you're waiting on, kinda like a garden path sentence.

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.

This implies that there are never any inside an expression, which is a restriction I absolutely wouldn't support, given Rust's expression-oriented nature. And at least with C#'s await, it's absolutely plausible to have CallSomething(argument, await whatever.Foo().

Given that async will appear in the middle of expressions, I don't understand why it's easier to see in prefix than it would be in postfix.

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.

return (and continue and break) and while are notable as completely useless to chain, as they always return ! and (). And while for some reason you omitted for, we've seen code written just fine using .for_each() without bad effects, particularly in rayon.

traviscross commented 5 years ago

We probably need to make peace with the fact that async/await is going to be a major language feature. It will appear in all kinds of code. It will pervade the ecosystem -- in some places it will be as common as ?. People will have to learn it.

Consequently, we might want to consciously focus on how the syntax choice will feel after using it for a long time rather than how it will feel at first.

We also need to understand that as far as control flow constructs go, await is a different kind of animal. Constructs like return, break, continue, and even yield can be understood intuitively in terms of jmp. When we see these, our eyes bounce across the screen because the control flow we care about is moving elsewhere. However, while await affects the control flow of the machine, it doesn't move the control flow that's important to our eyes and to our intuitive understanding of the code.

We're not tempted to chain unconditional calls to return or break because that would make no sense. For similar reasons, we're not tempted to change our precedence rules to accommodate them. These operators have low precedence. They take everything to the right and return it somewhere, ending execution within that function or block. The await operator, however, wants to be chained. It's an integral part of an expression, not the end of it.

Having considered the discussion and examples in this thread, I'm left with the gnawing sense that we would live to regret surprising precedence rules.

The stalking horse candidate seems to be going with await!(expr) for now and hoping something better is worked out later. Prior to reading @Centril's remarks, I probably would have supported this in the interest of getting this important feature out with nearly any syntax. However, his arguments convince me this would just be copping out. We know that method call chaining is important in Rust. That drove the adoption of the ? keyword, which is widely popular and wildly successful. Using a syntax that we know will disappoint us is indeed just adding technical debt.

Early in this thread, @withoutboats indicated that only four existing options seem viable. Of those, only the postfix expr await syntax is likely to make us happy long-term. This syntax doesn't create strange precedence surprises. It doesn't force us to create a prefix version of the ? operator. It works nicely with method chaining and doesn't break up left-to-right control flow. Our successful ? operator serves as precedent for a postfix operator, and await is more like ? in practice than it is like return, break, or yield. While a postfix non-symbol operator may be new in Rust, usage of async/await will be widespread enough to make it quickly familiar.

While all of the options for a postfix syntax seem workable, expr await has some advantages. This syntax makes it clear that await is a keyword, which helps to emphasize the magic control flow. Compared with expr.await, expr.await() expr.await!, expr.await!(), etc., this avoids having to explain that this looks like a field/method/macro, but really isn't in this one special case. We would all get used to the space separator here.

Spelling await as @ or using some other symbol that doesn't cause parsing problems is appealing. It's certainly an important enough operator to warrant it. But if, in the end, it has to be spelled await, that will be fine. As long as it's in postfix position.

mehcode commented 5 years ago

As someone mentioned real examples ... I maintain a (according to tokei) 23,858 line rust codebase that is very heavily async and uses futures 0.1 await (highly experimental I know). Let's go (redacted) spelunking (note everything has been run through rustfmt):

// A
if !await!(db.is_trusted_identity(recipient.clone(), message.key.clone()))? {
    info!("recipient: {}", recipient);
}

// B
match await!(db.load(message.key))? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = await!(client
    .get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send())?
.error_for_status()?;

// D
let mut res =
    await!(client.get(inbox_url).headers(inbox_headers).send())?.error_for_status()?;

let mut res: InboxResponse = await!(res.json())?;

// E
let mut res = await!(client
    .post(url)
    .multipart(form)
    .headers(headers.clone())
    .send())?
.error_for_status()?;

let res: Response = await!(res.json())?;

// F
#[async]
fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let (_, mut res) = await!(self.request(url, Method::GET, None, true))?;
    let user = await!(res.json::<UserResponse>())?
        .user
        .into();

    Ok(user)
}

Now lets transform this into the most popular prefix variant, obvious precedence with sugar. For obvious reasons this has not been run through rustfmt so apologies if there is a better way to write it.

// A
if await? db.is_trusted_identity(recipient.clone(), message.key.clone()) {
    info!("recipient: {}", recipient);
}

// B
match await? db.load(message.key) {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = (await? client
    .get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send())
.error_for_status()?;

// D
let mut res =
    (await? client.get(inbox_url).headers(inbox_headers).send()).error_for_status()?;

let mut res: InboxResponse = await? res.json();

// E
let mut res = (await? client
    .post(url)
    .multipart(form)
    .headers(headers.clone())
    .send())
.error_for_status()?;

let res: Response = await? res.json();

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let (_, mut res) = await? self.request(url, Method::GET, None, true);
    let user = (await? res.json::<UserResponse>())
        .user
        .into();

    Ok(user)
}

Finally, lets transform this into my favorite postfix variant, "postfix field".

// A
if db.is_trusted_identity(recipient.clone(), message.key.clone()).await? {
    info!("recipient: {}", recipient);
}

// B
match db.load(message.key).await? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .send().await?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send().await?
    .error_for_status()?
    .json().await?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send().await?
    .error_for_status()?
    .json().await?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.request(url, Method::GET, None, true).await?
        .res.json::<UserResponse>().await?
        .user
        .into();

    Ok(user)
}

After this exercise, I find a number of things.

richardanaya commented 5 years ago

I appreciate the ellipses postfix notation "..." above , both for its conciseness and symbolically in English language representing a pause in anticipation of something else. (Like how async behavior works!), it also chains together well.

let resultValue = doSomethingAndReturnResult()...?;
let resultValue = doSomethingAndReturnResult()...?.doSomethingOnResult()...?;
let value = doSomethingAndReturnValue()....doSomethingOnValue()...;
let arrayOfValues = vec![doSomethingA(),doSomethingB()]...?;
// Showing stacking
let value = doSomethingWithVeryLongFunctionName()...?
                 .doSomethingWithResult()...?;

I doubt any other options will be as concise and visually meaningful.

mokeyish commented 5 years ago

A

let mut res: Response = (await client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send().await?
    .error_for_status()?
    .json())?;

B

let mut res: Response = await client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .send().await?
    .error_for_status()?
    .json());
let res = res.unwrap();
novacrazy commented 5 years ago

Should it even be considered good form to have long chains of awaits?

Why not simply use regular Future combinators?

In fact, some expressions don't translate well into await chains if you want to have backup behaviors on failure.

Personally I think this:

let value = await some_op()
                 .and_then(|v| v.another_op())
                 .and_then(|v2| v2.final_op())
                 .or_else(|| backup_op());

value.unwrap()

reads far more naturally than this:

let value = match await some_op() {
    Ok(v) => match await v.another_op() {
        Ok(v2) => await v2.final_op(),
        Err(_) => await backup_op(),
    },
    Err(_) => await backup_op(),
};

value.unwrap()

We still have the full power of zero-cost futures at our hands, after all.

Consider that.

earthengine commented 5 years ago

@EyeOfPython I would like to hightlight that we have another choice than @ in future@await. We can write future~await, where the ~ is worked like a semi hyphen, and would work for any possible postfix operators.

The hyphen - was already used as the minus operator and negative operator. No longer good. But ~ was used to indicate heap objects in Rust, and otherwise it was hardly used in any programming languages. It sould gives less confusions for people from other languages.

dyxushuai commented 5 years ago

@earthengine Good idea, but maybe just use future~ which means await a future, where the ~ is worked like the await keyword.(like ? symbol

Future Future of Result Result of Future
future~ future~? future?~

Also chained futures like:

let res: MyResponse = client.get("https://my_api").send()~?.json()~?;
I60R commented 5 years ago

I've take a look at how Go implements async programming and found something interesting there. The closest alternative to futures in Go are channels. And instead of await or other screaming syntax to wait for values Go channels just provides <- operator for that purpose. For me it looks pretty clean and straightforward. Previously I've often seen how people praises Go for its simple syntax and good async facilities, so it's definitely a good idea to learn something from its experience.

Unfortunatelly, we couldn't have exactly the same syntax because there's a lot more angle braces in Rust source code than in Go, mostly because of generics. This makes <- operator really subtle and not pleasant to work with. Another disadvantage is that it could be seen as opposite to -> in function signature and there's no reason to consider it like that. And yet another disadvantage is that <- sigil was intended to be implemented as placement new, so people could misinterpret it.

So, after some experiments with syntax I stopped at <-- sigil:

let output = <-- future;

In async context <-- is pretty straightforward, although less than <-. But instead it provides a big advantage over <- as well as over prefix await - it plays well with indentation.

async fn log_service(&self) -> T {
   let service = self.myService.foo();
   <-- self.logger.log("beginning service call");
   let output = <-- service.exec();
   <-- self.logger.log("foo executed with result {}.", output));
   output
}

async fn try_log(message: String) -> Result<usize, Error> {
    let logger = <-- acquire_lock();
    let length = <-- logger.log_into(message)?;
    <-- logger.timestamp();
    Ok(length)
}

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

For me this operator looks even more unique and easier to spot than await (which looks more like any other keyword or variable in code context). Unfortunately, on Github it looks more thin than in my code editor but I think that's not fatal and we can live with that. Even if someone feels uncomfortable, different syntax highlighting or better font (especially with ligatures) will resolve all of the problems.

Another reason to like this syntax is because it conceptually could be expressed as "something that's not yet here". Right to left direction of arrow is opposite to the direction how we read text, which allows us to describe it as "thing that comes from future". The long shape of <-- operator also suggests that "some durable operation begins". Angle bracket and two hyphens directed from future could symbolize "continuous polling". And we still able to read it as "await" like it was before.
Not some kind of enlightenment, but might be fun.


The most important thing in this proposal is that ergonomic method chaining also would be possible. The idea of delayed prefix operator which I've proposed previously is a good fit here. In this way we would have the best from both worlds of prefix and postfix await syntax. I really hope that there also would be introduced some useful extras which personally I wanted in many occasions before: delayed dereferencing and delayed negation syntax.

Through, I'm not sure if delayed word is proper here, maybe we should name it differently.

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

let not_empty = some_vec.!is_empty();

let deref = value.*as_ref();

Operator precedence looks pretty obvious: from left to right.

I hope this syntax will reduce need in writing is_not_* functions which purpose is only to negate and return a bool property. And boolean/dereferencing expressions in some cases will be more cleaner when using it.


Finally, I've applied it on real world examples posted by @mehcode and I like how <-- makes proper emphasis on async function inside of method call chains. Contrarily, postfix await just looks like regular field access or function call (depending on syntax) and it's nearly impossible to distinguish them between without special syntax highlighting or formatting.

// A
if db.<--is_trusted_identity(recipient.clone(), message.key.clone())? {
    info!("recipient: {}", recipient);
}

// B
match db.<--load(message.key)? {
    Some(key) => key,
    None => {
        return Err(/* [...] */);
    }
};

// C
let mut res = client.get(&script_src)
    .header("cookie", self.cookies.read().as_header_value())
    .header("user-agent", USER_AGENT)
    .<--send()?
    .error_for_status()?;

// D
let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .<--send()?
    .error_for_status()?
    .<--json()?;

// E
let mut res: Response = client.post(url)
    .multipart(form)
    .headers(headers.clone())
    .<--send()?
    .error_for_status()?
    .<--json()?;

// F
async fn request_user(self, user_id: String) -> Result<User> {
    let url = format!("users/{}/profile", user_id);
    let user = self.<--request(url, Method::GET, None, true)?
        .res.<--json::<UserResponse>()?
        .user
        .into();

    Ok(user)
}

After all: that's the syntax I want to use.

HeroicKatora commented 5 years ago

@novacrazy

Should it even be considered good form to have long chains of awaits? Why not simply use regular Future combinators?

I'm not sure if I didn't misunderstand you but those are not exclusive, you can still use combinators as well

let value = some_op()
    .and_then(|v| v.another_op())
    .and_then(|v2| v2.final_op())
    .or_else(|| backup_op())
    .await;

But for this to work, the and_then clause needs to be typed at FnOnce(T) -> impl Future<_>, not only at FnOnce(T) -> U. Doing chained combinators on the future and the result only works cleanly without paranthesis in postfix:

let result = load_local_file()
    .or_else(|_| request_from_server()) // Async combinator
    .await
    .and_then(|body| serde_json::from_str(&body)); // Sync combinator
HeroicKatora commented 5 years ago

In this post I'll focus on the question of operator precedence in the post-fix case. As far as I can tell, we have three viable alternatives that at least work to some extent, each exemplified by one post-fix expression that has these precedences in current syntax. I feel strongly that none of the proposals should change current operator precedence.

The differences in functionality are rather small but existent. Note that I would accept all of these, this is mostly finetuning. To show them all, we need some types. Please don't comment on the contrivedness of the example, this is the most compressed version that shows all differences at once.

struct Foo<A, F, S> where A: Future<Output=F>, F: FnOnce(usize) -> S {
    member: A,
}

// What we want to do, in macro syntax:
let foo: Foo<_, _, _> = …;
(await!(foo.member))(42)

All of them look the same when we neither destructure an input struct through move of a member, nor call the result as a callable since these three precedence classes come directly after one another. How do we want futures to behave?

The best parallel to method call should be just another method call taking self.

The best parallel to member is a struct that only has the implicit member await and thus is destructured by moving from this, and this move implicitely awaits the future. This one feels like the least obivous.

The parallel to function is call is the behaviour of closures. I'd prefer this solution as the cleanest addition to the language corpus (as in syntax best parallels the possibilities of the type) but are some positive points for method call and .await is never longer than the others.

Centril commented 5 years ago

@HeroicKatora We could special case .await in libsyntax to allow for foo.await(42) but this would be inconsistent / ad-hoc. However, (foo.await)(42) seems serviceable since while futures outputting closures exist, they are probably not all that common. Thus if we optimize for the common case, not having to add () to .await likely wins out on balance.

HeroicKatora commented 5 years ago

@Centril I agree, consistency is important. When I see a couple of behaviours that are similar, I'd like to infer others through synthesis. But here .await seems awkward, especially with the above example clearly showing that it parallels implicit destructuring (unless you can find a different syntax where these effects occur?). When I see destructuring, I instantly wonder whether I can use this with let-bindings etc. This, however, would not be possible because we'd destructure the original type that has no such member or especially when the type is just impl Future<Output=F> (Mostly irrelevant sidenote: making this work would brings us back to an alternative prefix await _ = in the place of let _ =, funnily¹).

That doesn't forbid us from using the syntax per-se, I think I could deal with learning it and if it turns out to be the final one I will use it with vigor, but it seems like a clear weakness.


¹ This could be consistent while allowing ? by permitting ? behind names in a pattern

to match the Ok part of a Result. This seems interesting to explore in other contexts as well but rather off-topic. It would also lead to matching prefix and suffix syntax for await at the same time.

Centril commented 5 years ago

That doesn't forbid us from using the syntax per-se, I think I could deal with learning it and if it turns out to be the final one I will use it with vigor, but it seems like a clear weakness.

I think every solution will have some drawback in some dimension or case re. consistency, ergonomics, chainability, readability, ... This makes it a question of degree, importance of the cases, fitness to typical Rust code and APIs, etc.

In the case of a user writing foo.await(42)...

struct HasClosure<F: FnOnce(u8)> { closure: F, }
fn _foo() {
    let foo: HasClosure<_> = HasClosure { closure: |x| {} };

    foo.closure(42);
}

...we already provide good diagnostics:

5 |     foo.closure(42);
  |         ^^^^^^^ field, not a method
  |
  = help: use `(foo.closure)(...)` if you meant to call the function stored in the
          `closure` field

Tweaking this to fit foo.await(42) seems quite attainable. In fact, as far as I can see, we know that the user intends (foo.await)(42) when foo.await(42) is written so this can be cargo fixed in a MachineApplicable manner. Indeed, if we stabilize foo.await but don't allow foo.await(42) I believe we can even change the precedence later if we need to since foo.await(42) won't be legal at first.

Further nestings would work (e.g. future of result of closure -- not that this will be common):


struct HasClosure<F: FnOnce(u8)> { closure: Result<F, ()>, }
fn _foo() -> Result<(), ()> {
    let foo: HasClosure<_> = HasClosure { closure: Ok(|x| {}) };

    foo.closure?(42);

    Ok(())
}
HeroicKatora commented 5 years ago

e.g. future of result of closure -- not that this will be common

The extra suffix ? operator makes this unambiguous without modifications to syntax–in any of the post-fix examples. No tweaking necessary. The problems are only to .member explicitely being a field and the need for move-destructuring to apply first. And I really don't want to say that this would be hard to write. I mostly want to say that this seem inconsistent with other .member uses which e.g. can be transformed to matching. The original post was weighing positives and negatives in that regard.

Edit: Tweaking to fit future.await(42) has the, likely unintended, extra risk of making this a) inconsistent with closures where this is not the case due to methods of the same name as member being allowed; b) inhibiting future developments where we'd like to give arguments to await. But, as you previously mentioned, tweaking for Future returning a closure should not be the most pressing issue.

Pauan commented 5 years ago

@novacrazy Why not simply use regular Future combinators?

I'm not sure how much experience you have with Futures 0.3, but the general expectation is that combinators won't be used much, and the primary/idiomatic usage will be async/await.

Async/await has several advantages over combinators, e.g. it supports borrowing across yield points.

Combinators existed long before async/await, but async/await was invented anyways, and for good reason!

Async/await is here to stay, which means that it needs to be ergonomic (including with method chains).

Of course people are free to use the combinators if they wish, but they shouldn't be necessary in order to get good ergonomics.

As @cramertj said, let's try to keep the discussion focused on async/await, not alternatives to async/await.

In fact, some expressions don't translate well into await chains if you want to have backup behaviors on failure.

Your example can be simplified significantly:

let value = try {
    let v = await some_op()?;
    let v2 = await v.another_op()?;
    await v2.final_op()?
};

match value {
    Ok(value) => Ok(value),
    Err(_) => await backup_op(),
}.unwrap()

This makes it clear which parts are handling the error, and which parts are on the normal happy path.

This is one of the great things about async/await: it works well with other parts of the language, including loops, branches, match, ?, try, etc.

In fact, aside from await, this is the same code you would write if you weren't using Futures.

Another way to write it, if you prefer using the or_else combinator:

let value = await async {
    try {
        let v = await some_op()?;
        let v2 = await v.another_op()?;
        await v2.final_op()?
    }
}.or_else(|_| backup_op());

value.unwrap()

And best of all is to move the normal code into a separate function, making the error handling code even clearer:

async fn doit() -> Result<Foo, Bar> {
    let v = await some_op()?;
    let v2 = await v.another_op()?;
    await v2.final_op()
}
let value = await doit().or_else(|_| backup_op());

value.unwrap()
Pauan commented 5 years ago

(This is a reply to @joshtriplett's comment).

To be clear, you do not have to parenthesize, I mentioned it because some people said that it's too hard to read without the parentheses. So the parentheses are an optional stylistic choice (useful only for complex one-liners).

All of the syntaxes benefit from parentheses in some situations, none of the syntaxes are perfect, it's a question of which situations we want to optimize for.

Also, after re-reading your comment, perhaps you thought I was advocating for prefix await? I wasn't, my example was using postfix await. I overall like postfix await, though I like some of the other syntaxes too.

EyeOfPython commented 5 years ago

I'm starting to get comfortable with fut.await, I think people's initial reaction will be "Wait, that's how you do await? Weird." but later they'd love it for the convenience. Of course, the same is true for @await, which stands out much more than .await.

With that syntax, we can leave out some of the lets in the example:

`.await` `@await`
```rust let value = try { some_op().await? .another_op().await? .final_op().await? }; match value { Ok(value) => Ok(value), Err(_) => backup_op().await, }.unwrap() ``` ```rust let value = try { some_op()@await? .another_op()@await? .final_op()@await? }; match value { Ok(value) => Ok(value), Err(_) => backup_op()@await, }.unwrap() ```

This also makes clearer what's getting unwrapped with ?, for await some_op()?, it's not obvious whether some_op() gets unwrapped or the awaited result.

novacrazy commented 5 years ago

@Pauan

I’m not trying to shift focus away from the topic here, I’m trying to point out it doesn’t exist in a bubble. We have to consider how things work together.

Even if the ideal syntax was chosen, I’d still want to use custom futures and combinators in some situations. The idea that those could be soft deprecated makes me question the entire direction of Rust.

The examples you give still look terrible compared to the combinators, and with the generator overhead will probably be a bit slower and produce more machine code.

As far as await goes, all this prefix/postfix sigil/keyword bikeshedding is wonderful, but perhaps we should be pragmatic and go with the simplest option that is most familiar to users coming to Rust. I.e.: prefix keyword

This year is going to go by faster than we think. Even January is mostly done. If it turns out that users aren’t happy with a prefix keyword, it can be changed in a 2019/2020 edition. We can even make a “hindsight is 2020” joke.

CAD97 commented 5 years ago

@novacrazy

General consensus I've seen is that the earliest we'd want a third edition is 2022. We definitely don't want to plan around another edition; the 2018 edition was great but not without its costs. (And one of the points of the 2018 edition is making async/await possible, imagine taking that back and saying "nope, you need to upgrade to the 2020 edition now!")

In any case, I don't think that a prefix keyword -> postfix keyword transition is possible in an edition, even if it would be desirable. The rule around editions is that there needs to be a way to write idiomatic code in edition X such that it compiles without warnings and works the same in edition X+1.

It's the same reason that we'd rather not stabilize with a keyword macro if we can drive consensus on a different solution; deliberately stabilizing a solution we know is undesirable is itself problematic.

I think we've shown that a postfix solution is more optimal, even for expressions with only one await point. But I doubt that any of the proposed postfix solutions is obviously better than all the others.

BenoitZugmeyer commented 5 years ago

Just my two cents (I am a nobody, but I follow the discussion for quite a long time). My favorite solution would be the @await postfix version. Maybe you could consider a postfix !await, like some new postfix macro syntax?

Example:

let mut res: InboxResponse = client.get(inbox_url)
    .headers(inbox_headers)
    .send()!await?
    .error_for_status()?
    .json()!await?;

After a few language iterations, being able to implement our own postfix macros would be awesome.

dpc commented 5 years ago

... all new sygils proposals

Rust is already syntax/sygil heavy, we have await keyword reserved, stuff@await (or any other sygil) looks weird/ugly (subjective, I know), and is just ad-hoc syntax that does not integrate with anything else in the language which is a big red flag.

I've take a look at how Go implements ... <-- ... proposal

@I60R : Go has a terrible syntax full of ad-hoc solutions, and is totally imperative, very much unlike Rust. This proposal is again sygil/syntax heavy and totally ad-hoc just for this particular feature.

Centril commented 5 years ago

@I60R : Go has a terrible syntax

Let's please refrain from bashing other languages here. "X has terrible syntax" does not lead to enlightenment and to consensus.

cenwangumass commented 5 years ago

As a Python/JavaScript/Rust user and a computer science student, I personally prefer prefix await + f.await() to be both in the language.

  1. Both Python and JavaScript have prefix await. I would expect to see await appear in the beginning. If I have to read deep into line to realize this is asynchronous code, I feel very uneasy. With Rust's WASM capability, it might attract many JS developers. I believe familiarity and comfort is really important, considering Rust already has a lot of other new concepts.

  2. Postfix await seems convenient in chaining settings. However, I dislike solutions like .await, @await, f await because they look like ad-hoc solution to await syntax while it makes sense to think .await() as calling a method on the future.

Swoorup commented 5 years ago

Rust is already a departure from javascript and await is nothing like calling function(i.e functionality cant be emulated via a function) using functions to denote await makes it confusing to first timers being introduced to async-await. Hence i think the syntax should be different.

CAD97 commented 5 years ago

I've convinced myself that .await() is probably significantly more desirable over .await, though the rest of this post hedges that position a bit.

The reason for this is that await!(fut) has to consume fut by value. Making it look like a field access is bad, because that doesn't have the connotation of moving the way that a prefix keyword does, or the potential of moving like a macro or a method call does.

Interestingly, the keyword method syntax makes it almost look like an implicit await design. Unfortunately, "Explicit Async, Implicit Await" isn't possible for Rust (so please don't re-litigate it on this thread) since we want async fn() -> T to be used identically to fn() -> Future<T>, rather than activate an "implicit await" behavior.

The fact that a .await() syntax looks like an implicit await system (like Kotlin uses) could be a detractor, and would almost give the feel of an "Implicit Async, Implicit Await" due to the magic around the not-really-a-method-call .await() syntax. Would you be able to use that as await(fut) with UFCS? Would it be Future::await(fut) for UFCS? Any syntax that looks like another dimension of the language raises problems unless it can be somewhat unified with it at least syntactically, even if not functionally.

I remain skeptical if the benefits of any individual postfix solution outweigh the drawbacks of that same solution, though the concept of a postfix solution is more desirable than a prefix one generally.

Matthias247 commented 5 years ago

I'm a bit surprised that this thread is full of suggestions that seem to be made because they are possible - and not because they seem to yield any significant benefits over the initial suggestions. Can we stop talking about $, #, @, !, ~ etc, without bringing up a significant argument what is wrong with await, which is well understand and has proven itself in various other programming languages?

I think the post from https://github.com/rust-lang/rust/issues/57640#issuecomment-455361619 already listed all good options.

From those:

For these reasons I prefer the "useful precedence" followed by "mandatory delimiters"

I60R commented 5 years ago

Go has a terrible syntax full of ad-hoc solutions, and is totally imperative, very much unlike Rust. This proposal is again sygil/syntax heavy and totally ad-hoc just for this particular feature.

@dpc, if you read <-- proposal completely, you would see that this syntax is only inspired by Go, however is pretty different and usable either in imperative and in function chaining context. I also fail to see how await syntax isn't ad-hoc solution, for me it's way more specific and way more clumsy than <--. It's similar to have deref reference/reference.deref/etc instead of *reference, or have try result/result.try/etc instead of result?. I either don't see any advantage of using await keyword other than familiarity with JS/Python/etc which anyway should be less significant than having consistent and composable syntax. And I don't see any disadvantage of having <-- sigil other than it's not an await which anyway isn't that simple as plain English and users should understand what it does first.

Edit: this as well could be a good answer to @Matthias247 post, since it provides some arguments against await and proposes a possible alternative not affected with the same problems


It's really interesting for me, through, to read critique against <-- syntax, free from arguments that appeals to historical and prejudiced reasons.

CAD97 commented 5 years ago

Let's mention actual specifics around precedence:

The precedence chart as it stands today:

Operator/Expression Associativity
Paths  
Method calls  
Field expressions left to right
Function calls, array indexing  
?  
Unary - * ! & &mut  
as left to right
* / % left to right
+ - left to right
<< >> left to right
& left to right
^ left to right
\| left to right
== != < > <= >= Require parentheses
&& left to right
\|\| left to right
.. ..= Require parentheses
= += -= *= /= %= &= \|= ^= <<= >>= right to left
return break closures  

Useful precedence puts await before ? so that it binds more tightly than ?. A ? in the chain thus binds an `await to everything before it.

let res = await client
    .get("url")
    .send()?
    .json();

Yes, with useful precedence, that "just works". Do you know what that does at a glance? Is that bad style (probably)? If so, can rustfmt fix that automatically?

Obvious precedence puts await somewhere below ?. I'm not sure exactly where, though those specifics probably don't matter too much.

let res = await? (client
    .get("url")
    .send())
    .json();

Do you know what that does at a glance? Can rustfmt put it into a useful style that isn't too wasteful of vertical and horizontal space automatically?


Where would postfix keywords fall in this? Probably Keyword Method with Method calls and Keyword Field with Field expressions, but I'm unsure how the others should bind. What options lead to the least possible configurations where the await receives a surprising "argument"?

For this comparison, I suspect "mandatory delimiters" (what I called Keyword Function in the summary) wins easily, as it would be equivalent to a normal function call.

mehcode commented 5 years ago

@CAD97 To be clear, remember that .json() is also a future (at least in reqwests).

let res = await await client
    .get("url")
    .send()?
    .json()?;
let res = await? await? (client
    .get("url")
    .send())
    .json();

The more I play with converting complex rust expressions (even ones with only need for 1 await, however, note that in my 20,000+ future code base almost every single async expression is an await, directly followed by another await), the more I dislike prefix for Rust.

This is all because of the ? operator. No other language has a postfix control flow operator and await that are essentially always paired in real code.


My preference is still postfix field. As a postfix control operator, I feel it needs the tight visual grouping that future.await provides over future await. And comparing to .await()?, I prefer how .await? looks weird so it will be noticed and users won't assume it's a simple function (and thus won't ask why UFCS doesn't work).


As one more data point in favor of postfix, when this is stabilized, a rustfix to go from await!(...) to whatever we decide would be very much appreciated. I don't see how anything but the postfix syntax could be unambiguously translated without wrapping stuff in ( ... ) unnecessarily.

newpavlov commented 5 years ago

I think first we should answer the question "do we want to encourage await usage in chaining contexts?". I believe the prevalent answer is "yes", so it becomes a strong argument for postfix variants. While await!(..) will be the easiest to add, I believe we should not repeat the try!(..) story. Also I personally disagree with the argument that "chaining hides potentially costly operation", we already have a lot of chaining methods which can be very heavy, so chaining does not entail lazyness.

While the prefix await keyword will be the most familiar for users coming from other languages, I don't think we should make our decision based on it and instead we should concentrate on longer term, i.e. usability, convenience and readability. @withoutboats talked about "familiarity budget", but I strongly believe we should not introduce sub-optimal solutions just for familiarity sake.

Now we probably don't want two ways of doing the same thing, so we should not introduce both postfix and prefix variants. So let's say we've narrowed our options to postfix variants.

First let's start with fut await, I strongly dislike this variant, because it will seriously mess with how humans parse code and it will be a constant source of confusion while reading code. (Don't forget that code is mostly for reading)

Next fut.await, fut.await() and fut.await!(). I think the most consistent and less confusing variant will be the postfix macro one. I don't think it's worth to introduce a new "keyword function" or "keyword method" entity just to save a couple of characters.

Lastly sigil based variants: fut@await and fut@. I don't like the fut@await variant, if we introduce the sigil why bother with the await part? Do we have plans for future extensions fut@something? If not, it simply feels redundant. So I like the fut@ variant, it solves the precedence issues, code becomes easy to understand, write and read. Visibility issues can be solved by code highlighting. It will not be hard to explain this feature as "@ for await". Of course the biggest drawback is that we will pay for this feature from the very limited "sigil budget", but considering the importance of the feature and how often it will be used in async codebases, I believe it will be worth in a long run. And of course we can draw certain parallels with ?. (Though we will have to be prepared for Perl jokes from Rust critics)

In conclusion: in my opinion if we are ready to burden "sigil budget" we should go with fut@, and if not with fut.await!().

I60R commented 5 years ago

When talking about familiarity, I don't think that we should care too much about familiarity with JS/Python/C#, since Rust is in different niche and already looks differently in many things. Providing syntax similar to these languages is short term and low reward goal. Nobody will select Rust only for using familiar keyword when under the hood it works completely different.

But familiarity with Go matter, since Rust is in similar niche and even by philosophy it's more close to Go than to other languages. And despite all of prejudiced hate, one of the strongest points in them both is that they don't blindly copy features, but instead implements solutions that really have reason for.

IMO, in this sense <-- syntax is strongest here

Pzixel commented 5 years ago

With all that, let's remember that Rust expressions can result in several chained methods. Most languages tend to not do that.

I'd like to remind C# dev team experience:

The main consideration against C# syntax is operator precedence await foo?

This is something i do feel like i can comment on. We thought about precedence a lot with 'await' and we tried out many forms before setting on the form we wanted. One of the core things we found was that for us, and the customers (internal and external) that wanted to use this feature, it was rarely the case that people really wanted to 'chain' anything past their async call. In other words, people seemed to strongly gravitate toward 'await' being the most important part of any full-expression, and thus having it be near the top. Note: by 'full expression' i mean things like the expression you get at the top of a expression-statement, or hte expression on the right of a top level assign, or the expression you pass as an 'argument' to something.

The tendency for people to want to 'continue on' with the 'await' inside an expr was rare. We do occasionally see things like (await expr).M(), but those seem less common and less desirable than the amount of people doing await expr.M().

and

This is also why we didn't go with any 'implicit' form for 'await'. In practice it was something people wanted to think very clearly about, and which they wanted front-and-center in their code so they could pay attention to it. Interestingly enough, even years later, this tendency has remained. i.e. sometimes we regret many years later that something is excessively verbose. Some features are good in that way early on, but once people are comfortable with it, are better suited with something terser. That has not been the case with 'await'. People still seem to really like the heavy-weight nature of that keyword and the precedence we picked.

Is a good point against sigil instead of dedicated (key)word.

https://github.com/rust-lang/rust/issues/50547#issuecomment-388939886

You should really listen to the guys with millions of users.

So you don't want to chain anything, you just want to have several await's, and my experience is the same. Writing async/await code for more than 6 years, and I never wanted such a feature. Postfix syntax looks really alien and is considered to resolve a situation that is likely to never happen. Async call is really a bold thing so several awaits on single line is too heavy.

HeroicKatora commented 5 years ago

The tendency for people to want to 'continue on' with the 'await' inside an expr was rare. We do occasionally see things like (await expr).M(), but those seem less common and less desirable than the amount of people doing await expr.M().

That seems like a-posteriori analysis. Maybe one of the reasons why they don't continue is because it is extremely awkward to do so in prefix syntax (Comparable to not wanting to try! multiple times in a statement because that remains readable through operator ?). The above mostly considers (as far as I can tell) precendence, not position. And I would like to remind you that C# is not Rust, and trait members may change quite a bit the desire to call methods on results.

andreytkachenko commented 5 years ago

@I60R,

  1. I think familiarity is important. Rust is relatively new language and people will be migrating from other languages and if Rust will be looking familiar it will be easier for them to make a decision to choose the Rust.
  2. I am not big fun of chaining methods - it is much harder to debug long chains and I thing chaining is just complicating code readability and may be allowed only as additional option (like as macros .await!()). The prefix form will force developers to extract code into methods instead of chaining, like:
    let resp = await client.get("http://api")?;
    let body: MyResponse = await resp.into_json()?;

    into something like this:

    let body: MyResponse = await client.get_json("http://api")?;
Pzixel commented 5 years ago

That seems like a-posteriori analysis. Maybe one of the reasons why they don't continue is because it is extremely awkward to do so in prefix syntax. The above only considers precendence, not position. And I would like to remind you that C# is not Rust, and trait members may change quite a bit the desire to call methods on results.

No, it's about internal C# team experiments when it had both prefix/postfix/implicit forms. And I'm talking about my experience which is not just a habit where I'm unable to see postfix form pros.

skade commented 5 years ago

@mehcode Your example doesn't motivate me. reqwest consciously decides to make the initial request/response cycle and the subsequent handling of the response (body stream) seperate concurrent processes, hence they should be awaited twice, like @andreytkachenko shows.

reqwest could totally expose the following APIs:

let res = await client
    .get("url")
    .json()
    .send();

or

let res = await client
    .get("url")
    .send()
    .json();

(The latter being a simple sugar over and_then).

I find it troubling that a lot of the postfix examples here use this chain as an example, as reqwests and hypers best api decision is keeping these things separate.

I honestly believe that most postfix await examples here should be rewritten using combinators (and sugar if necessary) or be kept similar operations and awaited multiple times.