I donât have time to write a carefully-crafted post right now, so please forgive me jotting down a bunch of scattered thoughts!
Relationship between async fn and fn -> impl Future
I very much agree with @MajorBreakfast.
The âsynchronous preambleâ pattern (using -> impl Future with an inner async block) is likely to play a nontrivial role in async programming in Rust. For that reason alone, I think itâs very important that youâre able to move completely smoothly between async fn and fn -> impl Future.
One way we can make this smoother is to introduce a special lifetime, 'input, that bounds all input lifetimes (even if elided). Thus weâd have:
async fn foo(x: &Type1, y: &Type2) -> i32 {
/* body */
}
// equivalent to
fn foo(x: &Type1, y: &Type2) -> impl Future<Output = i32> + 'input {
async { /* body */ }
}
// equivalent to
fn foo<'a, 'b>(x: &'a Type1, y: &'b Type2) -> impl Future<Output = i32> + 'a + 'b {
async { /* body */ }
}
As a general rule, treating a new feature as pure sugar for existing features has a ton of benefits. It means there are no truly new feature interactions to consider, since each interaction is explained by the desugaring. It improves interoperability, since you can use the âexplicitâ and sugary forms interchangeably. And it provides a multi-level understanding of the feature, allowing you to think about it as a first-class concept most of the time, but âdrop downâ into the desugaring when helpful. (Closures are probably the best example of this today, although you cannot implement the closure traits in stable Rust yet.)
One other thing I would add: I think itâs important that you usually be able to use the most idiomatic (i.e. sugary) signature style when defining methods within traits. That means using e.g. impl Iterator and async fn. As things stand, there are limitations that prevent these forms from being âfully expressiveâ when using in trait definitions, but we should work to remove those limitations. Which brings us to the next topicâŚ
Naming anonymous types
I understand @rpjohnstâs perspective that async fn can be viewed as a type introduction form, but I have several reservations to treating it specially in this regard:
-
Every fn is a type introduction form, and over time I expect we will expose that fact more explicitly. Given that we want to be able to view an async fn as an fn, that means that async fn really introduces two types: the type of the function, and the type of the future it returns. I think itâs important that any design for naming the type take this into account, to avoid confusion in the future (if and when we expose the fn type more directly).
-
My perspective has always been that we will have abstract type for truly tricky cases, but in the common case just using impl Trait should suffice. (In particular, as I mention above, I want to be able to unreservedly recommend people use impl Trait in trait definitions, rather than having idioms vary by context). My assumption was that weâd have some ergonomic means of projecting the entire return type of a function, which would suffice for async fn and the majority of impl Trait returns (given that they usually encompass the entire return type); I think itâs fine to recommend abstract type only for the nested case (Vec<impl Trait> which I expect to be less common. This has already been discussed earlier on the thread with Output â I think we should continue exploring this space to find something that works for both async fn and impl Trait more generally.
-
More concretely, we could e.g. consider outputof(path_to_fn) as shorthand for <typeof(path_to_fn) as FnOnce>::Output. I think thereâs a lot of design space here.
Edit: as per @MajorBreakfastâs earlier points, this should probably be TypeOf and OutputOf, just like we have Self as a keyword in the âtype namespaceâ.
async at the end
Finally, I want to take up @lordanâs suggestion of writing async closer to the return type; I think itâs worth some serious thought. Some observations:
-
We could probably enable a slightly more precedented syntax this way: fn foo(&self) -> async i32 + Send. That is, we could allow + to be used within an async return type to impose additional bounds.
-
Writing it in the return type has a stronger connotation that this is a function returning an async value (sorry @rpjohnst!) And in particular, the jump from that to putting async immediately in the body feels very natural to me (and I wonder whether something similar holds for try).
-
If we did go this route, I think weâd want to strongly consider calling the trait Async rather than Future, for obvious reasons 
@withoutboats, Iâm sure youâve thought about this space before â what are the pros/cons as you see them?