why async fn in traits are hard
26 October 2019
After reading boat’s excellent post on asynchronous destructors,
I thought it might be a good idea to write some about async fn
in
traits. Support for async fn
in traits is probably the single most
common feature request that I hear about. It’s also one of the more
complex topics. So I thought it’d be nice to do a blog post kind of
giving the “lay of the land” on that feature – what makes it
complicated? What questions remain open?
I’m not making any concrete proposals in this post, just laying out
the problems. But do not lose hope! In a future post, I’ll lay out a
specific roadmap for how I think we can make incremental progress
towards supporting async fn in traits in a useful way. And, in the
meantime, you can use the async-trait
crate (but I get ahead of
myself…).
The goal
In some sense, the goal is simple. We would like to enable you to
write traits that include async fn
. For example, imagine we have
some Database
trait that lets you do various operations against a
database, asynchronously:
trait Database {
async fn get_user(
&self,
) -> User;
}
Today, you should use async-trait
Today, of course, the answer is that you should dtolnay’s
excellent async-trait
crate. This allows you to write
almost what we wanted:
#[async_trait]
trait Database {
async fn get_user(&self) -> User;
}
But what is really happening under the hood? As the crate’s documentation explains, this declaration is getting transformed to the following. Notice the return type.
trait Database {
fn get_user(&self) -> Pin<Box<dyn Future<Output = User> + Send + '_>>;
}
So basically you are returning a boxed dyn Future
– a future
object, in other words. This desugaring is rather different from what
happens with async fn
in other contexts – but why is that? The rest
of this post is going to explain some of the problems that async fn
in traits is trying to solve, which may help explain why we have a
need for the async-trait
crate to begin with!
Async fn normally returns an impl Future
We saw that the async-trait
crate converts an async fn
to something
that returns a dyn Future
. This is contrast to the async fn
desugaring
that the Rust compiler uses, which produces an impl Future
. For example,
imagine that we have an inherent method async fn get_user()
defined on
some particular service type:
impl MyDatabase {
async fn get_user(&self) -> User {
...
}
}
This would get desugared to something similar to:
impl MyDatabase {
fn get_user(&self) -> impl Future<Output = User> + '_ {
...
}
}
So why does async-trait
do something different? Well, it’s
because of “Complication #1”…
Complication #1: returning impl Trait
in traits is not supported
Currently, we don’t support -> impl Trait
return types in traits.
Logically, though, we basically know what the semantics of such a
construct should be: it is equivalent to a kind of associated type.
That is, the trait is promising that invoking get_user
will return
some kind of future, but the precise type will be determined by the
details of the impl (and perhaps inferred by the compiler). So, if
know logically how impl Trait
in traits should behave, what stops
us from implementing it? Well, let’s see…
Complication #1a. impl Trait
in traits requires GATs
Let’s return to our Database
example. Imagine that we permitted
async fn
in traits. We would therefore desugar
trait Database {
async fn get_user(&self) -> User;
}
into something that returns an impl Future
:
trait Database {
fn get_user(&self) -> impl Future<Output = User> + '_;
}
and then we would in turn desugar that into something that uses an associated type:
trait Database {
type GetUser<'s>: Future<Output = User> + 's;
fn get_user(&self) -> Self::GetUser<'_>;
}
Hmm, did you notice that I wrote type GetUser<'s>
, and not type GetUser
? Yes, that’s right, this is not just an associated type,
it’s actually a generic associated type. The reason for
this is that async fn
always capture all of their arguments – so
whatever type we return will include the &self
as part of it, and
therefore it has to include the lifetime 's
. So, that’s one
complication, we have to figure out generic associated types.
Now, in some sense that’s not so bad. Conceptually, GATs are fairly simple. Implementation wise, though, we’re still working on how to support them in rustc – this may require porting rustc to use chalk, though that’s not entirely clear. In any case, this work is definitely underway, but it’s going to take more time.
Unfortunately for us, GATs are only the beginning of the complications
around async fn
(and impl Trait
) in traits!
Complication #2: send bounds (and other bounds)
Right now, when you write an async fn
, the resulting future may or
may not implement Send
– the result depends on what state it
captures. The compiler infers this automatically, basically, in
typical auto trait fashion.
But if you are writing generic code, you may well want to need to
require that the resulting future is Send
. For example, imagine we
are writing a finagle_database
thing that, as part of its inner
working, happens to spawn off a parallel thread to get the current
user. Since we’re going to be spawning a thread with the result from
d.get_user()
, that result is going to have to be Send
, which means
we’re going to want to write a function that looks something like
this1:
fn finagle_database<D: Database>(d: &D)
where
for<'s> D::GetUser<'s>: Send,
{
...
spawn(d.get_user());
...
}
This example seems “ok”, but there are four complications
- First, we wrote the name
GetUser
, but that is something we introduced as part of “manually” desugaringasync fn get_user
. What name would the user actually use? - Second, writing
for<'s> D::GetUser<'s>
is kind of grody, we’re obviously going to want more compact syntax (this is really an issue around generic associated types in general). - Third, our example
Database
trait has only one async fn, but obviously there might be many more. Probably we will want to make all of themSend
orNone
– so you can expand a lot more grody bounds in a real function! - Finally, forcing the user to specify which exact async fns have to
return
Send
futures is a semver hazard.
Let me dig into those a bit.
Complication #2a. How to name the associated type?
So we saw that, in a trait, returning an impl Trait
value is
equivalent to introducing a (possibly generic) associated type. But
how should we name this associated type? In my example, I introduced
a GetUser
associated type as the result of the get_user
function. Certainly, you could imagine a rule like “take the name of
the function and convert it to camel case”, but it feels a bit hokey
(although I suspect that, in practice, it would work out just
fine). There have been other proposals too, such as typeof
expressions and the like.
Complication #2b. Grody, complex bounds, especially around GATs.
In my example, I used the strawman syntax for<'s> D::GetUser<'s>: Send
. In real life, unfortunately, the bounds you need may well get
more complex still. Consider the case where an async fn
has generic
parameters itself:
trait Foo {
async fn bar<A, B>(a: A, b: B);
}
Here, the future that results bar
is only going to be Send
if A: Send
and B: Send
. This suggests a bound like
where
for<A: Send, B: Send> { S::bar<A, B>: Send }
From a conceptual point-of-view, bounds like these are no problem. Chalk can handle them just fine, for example. But I think this is pretty clearly a problem and not something that ordinary users are going to want to write on a regular basis.
Complication #2c. Listing specific associated types reveals implementation details
If we require functions to specify the exact futures that are
Send
, that is not only tedious, it could be a semver
hazard. Consider our finagle_database
function – from its where
clause, we can see that it spawns out get_user
into a scoped
thread. But what if we wanted to modify it in the future to spawn off
more database operations? That would require us to modify the
where-clauses, which might in turn break our callers. Seems like a
problem, and it suggests that we might want some way to say “all
possible futures are send”.
Conclusion: We might want a new syntax for propagating auto traits to async fns
All of this suggests that we might want some way to propagate auto
traits through to the results of async fns explicitly. For example,
you could imagine supporting async
bounds, so that we might write
async Send
instead of just Send
:
pub fn finagle_database<DB>(t: DB)
where
DB: Database + async Send,
{
}
This syntax would be some kind of “default” that expands to explicit
Send
bounds both DB
and all the futures potentially returned by
DB
.
Or perhaps we’d even want to avoid any syntax, and somehow
“rejigger” how Send
works when applied to traits that contain async
fns? I’m not sure about how that would work.
It’s worth pointing out this same problem can occur with impl Trait
in return position2, or indeed any associaed
types. Therefore, we might prefer a syntax that is more general and
not tied to async
.
Complication #3: supporting dyn traits that have async fns
Now imagine that had our trait Database
, containing an async fn get_user
. We might like to write functions that operate over dyn Database
values. There are many reasons to prefer dyn Database
values:
- We don’t want to generate many copies of the same function, one per database type;
- We want to have collections of different sorts of databases, such as a
Vec<Box<dyn Database>>
or something like that.
In practice, a desire to support dyn Trait
comes up in a lot of examples
where you would want to use async fn
in traits.
Complication #3a: dyn Trait
have to specify their associated type values
We’ve seen that async fn
in traits effectively desugars to a
(generic) associated type. And, under the current Rust rules, when you
have a dyn Trait
value, the type must specify the values for all
associated types. If we consider our desugared Database
trait, then,
it would have to be written dyn Database<GetUser<'s> = XXX>
. This is
obviously no good, for two reasons:
- It would require us to write out the full type for the
GetUser
, which might be super complicated. - And anyway, each
dyn Database
is going to have a distinctGetUser
type. If we have to specifyGetUser
, then, that kind of defeats the point of usingdyn Database
in the first place, as the type is going to be specific to some particular service, rather than being a single type that applies to all services.
Complication #3b: no “right choice” for X
in dyn Database<GetUser<'s> = X>
When we’re using dyn Database
, what we actually want is a type where
GetUser
is not specified. In other words, we just want to write
dyn Database
, full stop, and we want that to be expanded to
something that is perhaps “morally equivalent” to this:
dyn Database<GetUser<'s> = dyn Future<..> + 's>
In other words, all the caller really wants to know when it calls
get_user
is that it gets back some future which it can poll. It
doesn’t want to know exactly which one.
Unfortunately, actually using dyn Future<..>
as the type there is
not a viable choice. We probably want a Sized
type, so that the
future can be stored, moved into a box, etc. We could imagine then
that dyn Database
defaults its “futures” to Box<dyn Future<..>>
instead – well, actually, Pin<Box<dyn Future>>
would be a more
ergonomic choice – but there are a few concerns with that.
First, using Box
seems rather arbitrary. We don’t usually make Box
this “special” in other parts of the language.
Second, where would this box get allocated? The actual trait impl for
our service isn’t using a box, it’s creating a future type and
returning it inline. So we’d need to generate some kind of “shim impl”
that applies whenever something is used as a dyn Database
– this
shim impl would invoke the main function, box the result, and return
that.
Third, because a dyn Future
type hides the underlying
future (that is, indeed, its entire purpose), it also blocks the auto
trait mechanism from figuring out if the result is Send
. Therefore,
when we make e.g. a dyn Database
type, we need to specify not only
the allocation mechanism we’ll use to manipulate the future (i.e., do
we use Box
?) but also whether the future is Send
or not.
Now you see why async-trait desugars the way it does
After reviewing all these problems, we now start to see where the
design of the async-trait
crate comes from:
- To avoid Complications #1 and #2,
async-trait
desugarsasync fn
to return adyn Future
instead of animpl Future
. - To avoid Complication #3,
async-trait
chooses for you to use aPin<Box<dyn Future + Send>>
(you can opt-out from theSend
part). This is almost always the correct default.
All in all, it’s a very nice solution.
The only real drawback here is that there is some performance hit from boxing the futures – but I suspect it is negligible in almost all applications. I don’t think this would be true if we boxed the results of all async fns; there are many cases where async fns are used to create small combinators, and there the boxing costs might start to add up. But only boxing async fns that go through trait boundaries is very different. And of course it’s worth highlighting that most languages box all their futures, all of the time. =)
Summary
So to sum it all up, here are some of the observations from this article:
async fn
desugars to a fn returningimpl Trait
, so if we want to supportasync fn
in traits, we should also support fns that returnimpl Trait
in traits.- It’s worth pointing out also that sometimes you have to manually
desugar an
async fn
to afn
that returnsimpl Future
to avoid capturing all your arguments, so the two go hand in hand.
- It’s worth pointing out also that sometimes you have to manually
desugar an
- Returning
impl Trait
in a trait is equivalent to an associated type in the trait.- This associated type does need to be nameable, but what name should we give this associated type?
- Also, this associated type often has to be generic, especially
for
async fn
.
- Applying
Send
bounds to the futures that can be generated is tedious, grody, and reveals semver details. We probably some way to make that more ergonomic.- This quite likely applies to the general
impl Trait
case too, but it may come up somewhat less frequently.
- This quite likely applies to the general
- We do want the ability to have
dyn Trait
versions of traits that contain associated functions and/orimpl Trait
return types.- But currently we have no way to have a
dyn Trait
without fully specifying all of its associated types; in our case, those associated types have a 1-to-1 relationship with theSelf
type, so that defeats the whole point ofdyn Trait
. - Therefore, in the case of
dyn Trait
, we would want to have theasync fn
within returning some form ofdyn Future
. But we would have to effectively “hardcode” two choices:- What form of pointer to use (e.g.,
Box
) - Is the resulting future
Send
,Sync
, etc
- What form of pointer to use (e.g.,
- This applies to the general
impl Trait
case too.
- But currently we have no way to have a
The goal of this post was just to lay out the problems. I hope to
write some follow-up posts digging a bit into the solutions – though
for the time being, the solution is clear: use the async-trait
crate.
Footnotes
Astute readers might note that I’m eliding a further challenge, which is that you need a scoping mechanism here to handle the lifetimes. Let’s assume we have something like Rayon’s scope or crossbeam’s scope available. ↩︎
Still, consider a trait
IteratorX
that is likeIterator
, where the adapters returnimpl Trait
. In such a case, you probably want a way to say not only “I take aT: IteratorX + Send
” but also that theIteratorX
values returned by calls tomap
and the like areSend
. Presently you would have to list out the specific associated types you want, which also winds up revealing implementation details. ↩︎