I believe the issue is that the futures may capture self, therefore requiring a lifetime, which requires GATs. There were further complications described in why async fn in traits are hard such as how to deal with Send bounds, how to name the associated type, complex generic bounds, and supporting dyn Trait.
The problem of genericasync fns isn’t addressed. They’d be needing GATs (generic associated types). Also I don’t like the idea of having this be an actual desugaring with the effect of being able to name the returned future. It seems too inconsistent with ordinary async fns where you currently can’t name the returned future. At least one should thing about whether there couldn’t be any unified approach to naming function types and/or function return types. Especially having a type with the same name as a function but the type is (only) referring to the return type of that function seems somewhat weird.
Also I have a feeling that for generic async fn trait methods if they’re realized with GATs you still cannot really formulate constraints like e.g. “the returned future is Send whenever the function parameter is Send”, etc, with the current capabilities of Rust’s type system. And I somehow have the feeling that there might be situations where you want to formulate such a constraint.
Other than that, I do feel that—yes, of course—the kind you desugaring you suggest here is, in principle, the correct one. E.g. even without it being an actual desugaring with a namable associated type, if could still be treated “like an associated type” internally.
desugars to a type Foo and a function with the same name which returns the type Foo.
You are right, but I think this desugaring is forwards compatible with GATs and generic async functions in traits. I.e. when GATs become available, you'll be able to write
I'm not sure if this can even be expressed with Rusts type system. My intuition is that this would require dependent types. This proposal doesn't attempt to solve this problem, could you explain why it is relevant here?
I think currently the best solution would be to have two functions:
I think it’s relevant because ordinary async fns often (implicitly) have those properties as part of their public interface. If I write an async fn foo<T>(x: T) and then hold a value of type T over an await point, then the Future returned by foo::<T> has some kind of impl<T> Send for {future of foo::<T>} where T: Send implementation (and similar for Sync). It’s unfortunate enough that this status quo makes it way too easy to inadvertently introduce breaking changes for ordinary async fns, but if we tried to continue this “everything is implicit” approach with async methods in traits I think we’re going to get even more significant problems. It should probably work fine as long as traits are only used for “overloading” function calls with concrete types; but once you’re writing code that’s generic over a trait with async methods, you don’t get any information anymore about when Send etc. is implemented.
Your example with an explicit Send bound on a nameable return type does seem do demonstrate an approach that works for some generic cases. I haven’t got any experience or spent too much thoughts on what kinds of problems might arise in general. Maybe your approach even always works, but it might in any case involve a bunch of extra bounds on every generic function or method that’s involved. If a trait with a async fn foo<T>(x: T) method intends to only ever be implemented in such a way that T: Send implies {future of Self::foo::<T>}: Send, then having additional Self::foo<T>: Send bounds everywhere seems a bit inelegant.
Also, e.g. considering “prior art”, the async-trait crate seems to just unconditionally always require all the futures to be Send. That’s probably a compromise but also probably not for no reason.
Following my previous thoughts, I can think of two alternative approaches to consider
One could try to introduce some way of annotating Send and Sync implementations on async fns in a way that solves both the problem of “how does a trait with an async fn method specify under which conditions the returned future should be Send/Sync/etc” AND the problem of “how can I better avoid breaking changes in ordinary async fn”. Some kind of additional syntax for async fn that’s optional on ordinary async fn but mandatory with async trait methods.
One could also encourage a manual use of associated types and only allow the async fn syntax in the implementation. This way one could at least write something like
struct Bar;
impl MyTrait for Bar {
async fn some_generic_method<T: Send>(self, x: T) {
/* ... */
}
}
“desugaring” to, roughly,
impl MyTrait for Bar {
type SomeGenericMethodFuture<T: Send> = impl Future<Output=()>;
fn some_generic_method<T: Send>(self, x: T) {
async move {
let _ = self; // we want to make sure that *all* the arguments are
let _ = x; // being captured (for consistency with normal `async fn`)
/* ... */
}
}
}
of course, this kind of trait is then committed to only work for Self: Send and T: Send settings. It cannot specify the requirement “I only require the return type to implement Send if both Self and T implement Send”.
The problem is less about whether the proposal is forward compatible with GATs, and more that we need GATs to even write a useful implementation of this.
trait Database {
async fn get_user(&self) -> User;
}
// desugars to
impl MyDatabase {
// note the lifetime
fn get_user(&self) -> impl Future<Output = User> + '_;
}
// desugars to
trait Database {
// GAT
type GetUser<'s>: Future<Output = User> + 's;
fn get_user(&self) -> Self::GetUser<'_>;
}