The biggest pain point of using the async fn syntactic sugar in traits is the fact that there is no way of specifying Send bounds on its future. As @nikomatsakis wrote in its Return type notation (send bounds, part 2) · baby steps blog, one way of solving this problem could be to add support for adding bounds to the target generic, so that only types that a) implement Foo, and b) its desired functions implement the traits can be used. However, there is no way of specifying this at the trait level without desugaring, so the bounds are left to be added by the downstream users if they're needed (which often, they're either always needed, or never). Thus, projects like Tokio and its dependents might find themselves writing that where clause with said bounds on every generic, and even though Niko proposes trait aliases as a solution, my opinion is that this is a sub-par solution. My proposal tries to move the generic bounds from the user to the trait definition, by moving the new syntax to the trait, rather instead of the user.
For context, this is what Return Type Notation (RTN) would look like:
trait Foo {
async fn bar();
}
fn baz<T>(_foo: T)
where
T: Foo,
T::bar(): Send
{}
Every function that needs bar()
to return a Future + Send
will need to desugar what could be an impl Foo
into an explicit generic and a 2-lines where clause (unless we stabilize trait aliases).
My proposal is to allow these to be at the trait level without having to desugar into a manual impl Future...
. We could easily expand this feature to regular function in structs and modules, and it would serve as a bound to ensure that the Future returned always meets some invariants, which would be excellent for avoiding accidental semver breakages. I will attach some syntax bikeshed, because it is always easier to see it that way.
async fn foo() -> Bar: Send {}
async fn foo(): Send -> Bar {}
async: Send fn foo() -> Bar {}
async<Send> fn foo() -> Bar {}
It's the exact same amount of characters on all of them, so the discussion boils down to what we feel like fits better into the existing rust grammar. I ordered them according to my personal preference, from better to worst.
RTN is not mutually exclusive to this, and tackles a different problem. However, they overlap a bit and that is why I decided to mention it in this post and compare my proposal against it. RTN is still useful for traits that did not add these bounds because they are not necessary, but the user needs them to be there. I even have found that they would be very useful in a project of mine, but I digress.
In my mind, implementing a form of this proposal would be excellent for having async fn syntax not be an antipattern in public interfaces, and would allow more resilient APIs against accidental semver breakage.