Closed cramertj closed 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 .
@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.
@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 let
s and else
s, and for
loops. Once try { .. }
stabilizes I'd expect ?
to be used even more.
@ben0x539 I think we should reserve ...
for variadic generics, once we're ready to have them
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.
:( 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
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 .
@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.
@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
.
@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();
}
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):
await
await
a keyword or not?Other more drastic suggestions:
match
, if
, etc in the future to make this a generalized pattern, but is an unnecessary addendum to this debate) [reference]await
in patterns to resolve futures (no chaining at all) [reference]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.)
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.)
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
.
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.
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.
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.
I now am strongly against await? foo
. It reads nice for simple expressions but the ?
feels lost on complex expressions. If we must do prefix I'd rather have "useful" precedence.
Using postfix notation leads me to join statements and reduce unnecessary let bindings.
Using postfix field notation leads me to strongly prefer .await?
to appear in the line of the thing its await rather than on its own line in rustfmt parlance.
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.
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();
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.
@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.
@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()~?;
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.
@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
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.
future.await()
)future.await
)future(await)
)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)
foo.member.await()(42)
Binds most strongly, so no paranthesis at all(foo.member.await)(42)
Needs paranthesis around the awaited result when this is a callable, this is consistent with having a callable as a member, otherwise confusion with call to member function. This also suggests like one could destructure with patterns: let … { await: value } = foo.member; value(42)
somehow?(foo.member)(await)(42)
Needs paranthesis for destructuring (we move member) as it behaves as function call.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.
@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.
@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
await value? = failing_future();
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.
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 fix
ed 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(())
}
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.
@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()
(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.
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.
@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.
@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.
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.
... 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.
@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.
As a Python/JavaScript/Rust user and a computer science student, I personally prefer prefix await
+ f.await()
to be both in the language.
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.
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
.
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.
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.
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:
client.get("url").send() await?.json()?
. That whitespace in between looks out of place. With parentheses it would make a little more sense for me: (client.get("url").send() await)?.json()?
But I find the control flow still harder to follow than with the prefix variant.await
does a very complex operation - Rust has no computable properties and field access is otherwise a very simple operation. So it seems to give a wrong impression about the complexity of this operation. For these reasons I prefer the "useful precedence" followed by "mandatory delimiters"
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.
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.
@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.
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!()
.
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
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.
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.
@I60R,
.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")?;
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.
@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 reqwest
s and hyper
s 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.
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!)