I just got done reading @nikomatsakis's latest blogpost: http://smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard/.
This reminded me of an idea I had a while back but never really wrote down, which might go some way to solving Niko's "how do I require the future that this async trait function spits out is e.g. Send". A while back I proposed adding some syntax like MyFn::return
as a primitive for getting the return type of a function (imagine that MyFn
is a closure type, fnptr type, or the ZST associated with a fn item); I figured I'd sketch some responses to some of the problems listed in the blogpost, mostly "Complication #2".
I'm thinking of something like this: introduce new type syntax return
which, within the context of a fn
item (i.e., within its argument types, return type, bounds, body etc) behaves like a type alias for the function's return type (compare one of Centril's named existentials). This isn't a particularly novel idea, but consider the following:
async fn futz(x: impl T) -> usize
where return: Send;
I think the natural use of return
here is to refer to the synthesized impl Future
return type, which gives us a comparatively natural syntax for ensuring the future is Send[1]. This is also separately nice, because it gives us a nice name for the impl Future
that doesn't involve typeof
silliness. I certainly would prefer this mechanism to not be async
specific, for the benefit of generators and similar quasi-functions. Mind, this also isn't completely novel, since I remember some noise about writing async(Send) fn
or similar.
The key thing that I'm interested in, though, is the particularly painful GAT version[2] of this problem you get when you decide you need to bound the return type of an async trait method. While I imagine you could do something silly like
where <DB as Database>::get_user::return: Send
Ignoring all of the hilarious syntactic ambiguities, for the moment, I think a not-insane alternative is to require the trait to specify the Send
-ness and whatnot, i.e.,
trait Database {
async fn get_user(&self) -> User
where return: Send; // This bound is implied for all
// implementors of the trait, as usual.
}
This seems to solve the letter of Niko's problem, but certainly not the spirit of it. Now the trait declaration cares about Send
ness, which feels like the wrong choice somehow. This makes me wonder if instead we really do want Niko's async Send
bound? One could imaine something really silly like
where <DB as Database>::return: Send
where Trait::return: Send
is trying to say "for all return types of functions in Trait
, make sure they're Send
". Mind, this needs some way to specify the bound just for the async functions, not to mention that Trait::return
can't corrspond to any real type.
This also doesn't address Complication #2b. I don't really have any reasonable solutions other than a rough sketch. If we decided that "Send
is default", then
async fn foo<A, B>(&self);
should, presumably, implicitly carry where for<A: Send, B: Send> return: Send
. We can then imagine that where return: Send
upgrades this to where for<A, B> return: Send
, while where return: ?Send
deletes this where-clause altogether.
Looking back on this, I don't think I succeeded in addressing the problems as much as I might have hoped, and I got a little rambly, but hopefully something in this post inspires someone to say something a bit more intelligent than what I said.
[1] Mind, this is kind of obnoxious boilerplate, and what we really want is Send
by default with a return: ?Send
opt out, but that's kind of out in the rhubarb.
[2] From this point on, just assume we have GATs with all the semantics Niko typically uses for them.