Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upasync/await notation for ergonomic asynchronous IO #2394
Conversation
withoutboats
added
the
T-lang
label
Apr 6, 2018
sfackler
reviewed
Apr 6, 2018
|
|
||
| After gaining experience & user feedback with the futures-based ecosystem, we | ||
| discovered certain ergonomics challenges. Using state which needs to be shared | ||
| across await points was extremely ergonomic - requiring either Arcs or join |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment was marked as resolved.
This comment was marked as resolved.
|
Can we also ask for We can also reserve |
This comment was marked as outdated.
This comment was marked as outdated.
|
I'm curious to see an example where an async closure would be useful. I sort of expect async blocks to be the more common thing you want, but I'm probably overlooking something. In particular, I'm wondering why give the sugar-y syntax to async closures, and not async blocks -- or both. Were there complications around async blocks that led to them being deferred? (Obviously you can implement one in terms of the other, so it seems like the implementation work is more or less the same.) |
This comment was marked as outdated.
This comment was marked as outdated.
Put differently, when do you need a function producing a future rather than just a future, given that futures themselves are already "delayed" computations? I think this is a good question, and it suggests an alternative design where we don't have async closures, but do have async blocks, which can of course be used with closures anyway. |
This comment has been minimized.
This comment has been minimized.
|
The code generated for
One of the reasons to have the closures and not the blocks is that for the closures, nobody should find it surprising that they capture their environment and variables need to outlive the function body -- that's normal for functions returning closures. For a block, though, that behavior has no precedent. |
This comment has been minimized.
This comment has been minimized.
rpjohnst
commented
Apr 6, 2018
|
Awesome work putting this together! I think this is a great spot to land in the design space. It might be good, on some level, to frame This framing also maps the concept of an async closure to something like a Java anonymous inner class. In that sense, an async block is actually the closer of the two concepts to existing sync closures- it's an expression that evaluates to a value (one that impls |
This comment was marked as outdated.
This comment was marked as outdated.
It's useful in the same sorts of positions that existing closures are useful-- higher order functions and things that take arguments. For example, imagine a function which takes in an async function as a way to handle HTTP requests: fn main() {
serve_http(async |request| {
let body = await!(body)?;
let response = await!(some_async_computation(body))?;
Ok(response)
})
}However, I agree that async blocks are more generally useful. I think my personal preference leans slightly towards stabilizing async closures first, since they seem less controversial in design: |
This comment has been minimized.
This comment has been minimized.
squishy-clouds
commented
Apr 6, 2018
|
What is the reasoning behind using an |
Centril
reviewed
Apr 6, 2018
| A final - and extreme - alternative would be to abandon futures and async/await | ||
| as the mechanism for async/await in Rust and to adopt a different paradigm. | ||
| Among those suggested are a generalized effects system, monads & do notation, |
This comment has been minimized.
This comment has been minimized.
Centril
Apr 6, 2018
•
Contributor
First I want to say that I think this really well written (and readable) RFC.
I think it also lands very nicely.
In particular, my initial thinking here, is this RFC is forward-compatible with forms of effect polymorphism; which is quite nice.
One could imagine ?async as "possibly async", i.e: the callee decides on sync or async.
And ?const as "possibly const", -- again, the callee decides.
Then we can mix these and you get 4 different modes.
How useful all of this is an open question.
Going a bit further (or too far) effects may be try async which could be the same as the Result-embedded futures encoding?
Also, should async fn(T) -> R as a function type be a thing?
This comment was marked as off-topic.
This comment was marked as off-topic.
RalfJung
Apr 6, 2018
Member
Also, should async fn(T) -> R as a function type a thing?
Isn't that just fn(T) -> impl Future<Item=R>?
This comment was marked as off-topic.
This comment was marked as off-topic.
Centril
Apr 6, 2018
•
Contributor
That seemed to be the consensus on #rust-lang, yes =)
The question was more: should the surface syntax be a thing?
with analogy with const fn(T) -> R.
This comment was marked as off-topic.
This comment was marked as off-topic.
Centril
Apr 6, 2018
Contributor
Some further notes on the effect polymorphism angle (still noting that this is just "hypothetical"):
We could possibly interpret async fn foo(x: T) -> R {..} as the following:
effect<E> (E + async) fn foo(x: T) -> R {..}.
Which universally quantifies the ambient effect E and then you combine E + async.
But the body of foo can't assume anything about what E might be, so this should also be backwards compatible in the future with such as change?
This comment was marked as off-topic.
This comment was marked as off-topic.
RalfJung
Apr 6, 2018
•
Member
The question was more: should the surface syntax be a thing?
by analogy with const fn(T) -> R.
Well, very much unlike const, async can be just syntactic sugar.
This comment has been minimized.
This comment has been minimized.
|
I really love how this turned out (slightly disappointed that Surprisingly after how pro-"actual return type" I was I don't really mind how this looks. I think a big part of my revulsion with the current implementation is that the specific transform Despite liking the proposed return type treatment I still feel like the lack of being able to use named existential types is a downside; it removes the ability to further constrain a trait Client {
type Request<'a>: Future<Item = String> + 'a;
fn get(&mut self) -> Self::Request<'_>;
}
fn foo<C>(client: C) where C: Client, C::Request: Send {
...
} |
This comment has been minimized.
This comment has been minimized.
rpjohnst
commented
Apr 6, 2018
|
@Nemo157 The framing I described above, treating |
This comment was marked as outdated.
This comment was marked as outdated.
TyOverby
commented
Apr 6, 2018
|
For what its worth, the first time I read |
This comment has been minimized.
This comment has been minimized.
vorner
commented
Apr 6, 2018
|
I'm not sure I personally prefer yet another keyword that can be put in front of fn, over an attribute, but I guess opinions on that differ. Anyway, while it is a keyword, would it make sense to explicitly state its position in relation to other such keywords? For example, |
This comment has been minimized.
This comment has been minimized.
rpjohnst
commented
Apr 6, 2018
•
|
One bikeshed alongside the idea of This would make it more clear that you're not running any of the function body until you actually
Making call syntax available in In fact, tokio::run(async {
let a = async {
... await!(foo(1, 2, 3)) ...
};
let b = async {
... await!(bar("a", "b", "c")) ...
};
await!(join(a, b))
});This also has two future-proofing effects:
And an even smaller bikeshed: the list of things that |
This comment has been minimized.
This comment has been minimized.
|
@rpjohnst IMO the ergonomic difference between tokio::run(async {
let a = async {
... await!(foo(1, 2, 3)) ...
};
let b = async {
... await!(bar("a", "b", "c")) ...
};
await!(join!(a, b))
});and tokio::run(async {
await!(join!(foo(1, 2, 3), bar("a", "b", "c")))
});isn't worth whatever clarity benefits you gain from "forcing" |
This comment has been minimized.
This comment has been minimized.
rpjohnst
commented
Apr 6, 2018
•
|
Erm, I intended But you're right, it is a bit worse in that sense. On the other hand, it leaves open the option of implicit await, so it might look like this instead: tokio::run(async {
join(async { foo(1, 2, 3) }, async { bar("a", "b", "c") })
});Basically, annotate the uncommon case of not immediately awaiting, and leave the common case of immediate awaiting noise-free. |
This comment has been minimized.
This comment has been minimized.
That's a cool idea, but again it creates a distinction between Explicit |
This comment has been minimized.
This comment has been minimized.
egilburg
commented
Apr 7, 2018
•
|
If |
This comment has been minimized.
This comment has been minimized.
rpjohnst
commented
Apr 7, 2018
•
|
@cramertj Ah, that's a good point. Depending on how common we expect On the other hand, I'm not sure that's any different from just calling a sync function. There's always the possibility that you'll need to look up a function signature, and the fact that So while we don't need to decide the particular await syntax yet, it would still be nice to be forward compatible with implicit await (i.e. not directly expose "
|
This comment was marked as resolved.
This comment was marked as resolved.
comex
commented
Apr 7, 2018
|
Just a note that the syntax would have to be edition gated, since this compiles today: let async = true;
let _ = async || { true }; |
This comment was marked as resolved.
This comment was marked as resolved.
tmccombs
commented
Apr 7, 2018
•
|
If |
This comment was marked as outdated.
This comment was marked as outdated.
vorner
commented
Apr 7, 2018
|
@rpjohnst I would be against being await completely implicit. I find await much closer to try than to unsafe. While in unsafe, I need to be careful about everything (even outside of the block). With try, I want to see where exactly the function might exit, so I don't leave something in inconsistent state. It is a bit helped by RAII (eg. mutex is unlocked automatically), but I don't necessarily have to use RAII for all kinds of cleanups. With await, even RAII don't help (because the mutex will stay locked while I await out of the function). Having an explicit mark of some kind greatly helps here. Furthermore, if a dependency makes a breaking change and makes one of its functions from blocking to future and implicit awaiting is in place, it'll still compile, but introduce a hard to find subtle bug (eg. deadlocking from time to time). Therefore, in line with Rusts aim to help programmer be confident in the code, I think it is worth having an explicit annotation of some kind (not necessarily a macro or macro-like keyword or an operator...). |
scottmcm
reviewed
Apr 7, 2018
| 3. Define the precedence as the inconvient precedence - this seems equally | ||
| surprising as the other precedence. | ||
| 4. Introduce a special syntax to handle the multiple applications, such as | ||
| `await? future` - this seems very unusual in its own way. |
This comment has been minimized.
This comment has been minimized.
scottmcm
Apr 7, 2018
Member
- make
awaita postfix operator instead.
Every time I need (await Foo()).Bar in C# I'm sad and wish it were something postfix.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Centril
Apr 7, 2018
Contributor
Could you elaborate on why it should be postfix?
I think await Foo() reads better for an English (and at least Scandinavian) reader since you are (a)waiting for Foo() to complete.
This comment has been minimized.
This comment has been minimized.
rpjohnst
Apr 7, 2018
It solves the precedence confusion around ? and makes chaining easier. Basically the same arguments for going from try! to ?.
This comment has been minimized.
This comment has been minimized.
scottmcm
Apr 7, 2018
•
Member
@Centril I agree it looks better for simple cases, but once you have multiple or need to mix it with methods and fields, it gets worse. I think x.foo().await.bar().await.qaz (with highlighting) will look way better than (await (await x.foo()).bar()).qaz -- fewer hard-to-track-down parens and maintains the reading order.
(Prefix would probably be better in Lisp or Haskell, but postfix seems generally better in rust. I sometimes even wish we had no prefix operators, so there'd never be a !(foo?) vs (!foo)? confusion...)
Edit: to show the difference keyword highlighting would make for emphasizing await points:
x.foo().let.bar().qaz.yay().let.blah
This comment has been minimized.
This comment has been minimized.
vi
Apr 8, 2018
•
In any case future await ? should be added to the list as 5., without much analyzing if it is good or bad.
This comment has been minimized.
This comment has been minimized.
vi
Apr 8, 2018
•
Like with the ? instead of try!, it puts the actual thing (what we are calling?) at the first place, and boilerplate-y technicalities (yield point or regular call?) at the end.
This comment has been minimized.
This comment has been minimized.
tmccombs
Apr 8, 2018
I like the idea of a postfix await operator, but I'm not sure what the best syntax for that would be.
f.await looks too much like accessing a field f.await() looks too much like a method call f await is just as awkward to use as await f.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
HadrienG2
commented
Apr 7, 2018
|
@rpjohnst Would your concern about the syntax similarity between async and non-async fns leading to dropped futures be addressed by marking futures and streams as #[must_use], as is being discussed in the companion RFC? |
This comment has been minimized.
This comment has been minimized.
|
SGTM. I vaguely prefer @rpjohnst's implicit-
Calling a blocking function in async code would have already been a bug, wouldn't it? |
Nemo157
reviewed
Apr 7, 2018
| loop { | ||
| match Future::poll(Pin::borrow(&mut pin), &mut ctx) { | ||
| Async::Ready(item) => break item, | ||
| Async::Pending => yield, |
This comment was marked as resolved.
This comment was marked as resolved.
Nemo157
Apr 7, 2018
Contributor
As @glaebhoerl mentioned on the companion RFC the Async alias is not in the proposed API, these should be Poll::.
This comment has been minimized.
This comment has been minimized.
|
This comment has been minimized.
This comment has been minimized.
|
@MajorBreakfast The |
withoutboats
merged commit a9d6699
into
rust-lang:master
May 8, 2018
This comment has been minimized.
This comment has been minimized.
|
Per @aturon's comment on #2418, we are merging this RFC. This does not commit us to a particular futures API. That is left as an unresolved question until #2418 or a successor is merged. For now, #2418 is left as an open RFC. |
withoutboats
referenced this pull request
May 9, 2018
Open
Tracking issue for async/await (RFC 2394) #50547
This comment was marked as resolved.
This comment was marked as resolved.
jjpe
commented
May 9, 2018
•
|
Could somebody with write access update the rendered link to this? It is currently broken. Edit: Done. |
This comment has been minimized.
This comment has been minimized.
alexreg
commented
May 9, 2018
|
@withoutboats @Centril Does |
scottmcm
referenced this pull request
May 16, 2018
Closed
RFC: Reserve `throw` and `fail` as keywords in edition 2018 #2441
This comment has been minimized.
This comment has been minimized.
|
Cross-referencing: #2442 would allow a postfix await on an expression ( |
This comment has been minimized.
This comment has been minimized.
warlord500
commented
Jun 6, 2018
|
I am curious what is the difference between async Closure and async block? |
This comment has been minimized.
This comment has been minimized.
|
@warlord500 Async closures are like async functions in the regard that you need to call them to get a future. So, consistency is a reason. An async block is an expression that evaluates immediately to a future. We're also not sure whether they should allow using the |
This comment has been minimized.
This comment has been minimized.
|
For an example of where you would want to use each, async blocks are useful for having some synchronous immediately executed code when constructing a future: fn get(url: impl Into<Url>) -> impl Future<Output = Response> {
let url = url.into();
async {
// do the request
}
}while I mostly see async closures as useful for things like stream adaptors (this is including a few hypothetical apis, not sure how they'll really look) async fn sum_counts(urls: impl Iterator<impl Into<Url>>) -> Result<u32, Error> {
let counts = await!(stream::iter(urls).parallel(4).map(async |url| {
let res = await!(get(url))?;
let value: SomeStruct = await!(serde_json::from_async_reader(res.body()))?;
Ok(value.count)
}).collect::<Result<Vec<u32>>>())?;
Ok(counts.sum())
} |
This comment has been minimized.
This comment has been minimized.
Ekleog
commented
Aug 9, 2018
|
Some musing about the possibility of tail-call optimization with From what I guess, the compiler will not even try tail-call optimizing an The only scheme I can think of would be to wrap the result of the For trait-object-based handling, I think it's already possible to do this exclusively with a third-party crate, that would just have to provide the said wrapping future type. For enum-based handling, I more or less think it cannot possibly be done while remaining sane. It would first require the compiler to provide in some way the closure of all future types that can be called as tail-calls from a future, and then a crate to actually wrap it in a nice wrapping-future type. Overall, I think the potential gain (enum vs trait object dispatch) is not worth the added complexity. |
This comment has been minimized.
This comment has been minimized.
harrychin
commented
Oct 10, 2018
|
@Ekleog Just as some additional context, there was a suggestion from @rpjohnst to have a CPS-style transformation added to generators/futures which would allow futures to drive themselves which would lend itself well to TCO. https://internals.rust-lang.org/t/pre-rfc-cps-transform-for-generators/7120/23 It's hard to say whether that scheme would be "zero-cost", as the generators would be storing the pointers on the stack, but it would save cycles from having the CPU do extra polling and assist TCO. |
aturon
referenced this pull request
Nov 10, 2018
Merged
RFC: stabilize `std::task` and `std::future::Future` #2592
This comment has been minimized.
This comment has been minimized.
oleganza
commented
Nov 13, 2018
|
Re: syntax for Has anyone considered using I personally prefer spelled-out keywords ( |
This comment has been minimized.
This comment has been minimized.
|
@oleganza Yes, |
withoutboats commentedApr 6, 2018
•
edited by scottmcm
Rendered
Companion libs RFC
Edit: Updated rendered link to merged location