We've seen that async fn desugars to a regular function returning impl Future. But what happens when we have anotherimpl Trait inside the return value of the async fn?
#![allow(unused)]fnmain() {
traitDebugStream {
typenext<'me>: Future<Output = implDebug + 'me>
// ^^^^^// This lifetime wouldn't be here if not for// the `'_` in `impl Debug + '_`whereSelf: 'me;
fnnext(&mutself) -> Self::next<'_>;
}
}
which further desugars to:
#![allow(unused)]fnmain() {
traitDebugStream {
typenext<'me>: Future<Output = Self::next_0<'me>>
whereSelf: 'me;
typenext_0<'a>: Debug// ^^^^// This lifetime wouldn't be here if not for// the `'_` in `impl Debug + '_`whereSelf: 'a; // TODO is this correct?fnnext(&mutself) -> Self::next<'_>;
}
}
As we can see, this problem is more general than async fn. We'd like a solution to work for any case of nested impl Trait, including on associated types.
#![allow(unused)]fnmain() {
impl DebugStream for Factory {
typenext<'me> =
impl Future<Output = Option<implDebug + 'me>> + 'me// ^^^^^// This lifetime wouldn't be here if not for// the `'_` in `impl Debug + '_`whereSelf: 'me; // TODO is this correct?fnnext(&mutself) -> Self::next<'me>
{...}
}
}
which further desugars to:
#![allow(unused)]fnmain() {
impl DebugStream for Factory {
typenext<'me> =
impl Future<Output = Option<Self::next_0<'me>>> + 'mewhereSelf: 'me;
typenext_0<'a> = implDebug + 'a// ^^^^// This lifetime wouldn't be here if not for// the `'_` in `impl Debug + '_`whereSelf: 'a;
fnnext(&mutself) -> Self::next<'me>
{...}
}
}
As we saw before, the compiler generates a shim for our type's next function:
#![allow(unused)]fnmain() {
// Pseudocode for the compiler-generated shim that goes in the vtable.fncounter_next_shim(
this: &mut Counter,
) -> dynx Future<Output = Option<i32>> {
// We would skip boxing for #[dyn(identity)]let boxed = Box::pin(<Counter as BasicStream>::next(this));
<dynx Future>::new(boxed)
}
}
Now let's attempt to do the same thing for our original example. Here it is from above:
Generating the shim here is more complicated, because now it must do two layers of wrapping.
#![allow(unused)]fnmain() {
// Pseudocode for the compiler-generated shim that goes in the vtable.fnfactory_next_shim(
this: &mut Counter,
) -> dynx Future<Output = Option<i32>> {
let fut: impl Future<Output = Factory::next_0> =
<Factory as DebugStream>::next(this);
// We need to turn the above fut into:// impl Future<Output = dynx Debug>// To do this, we need *another* shim...structFactoryNextShim<'a>(Factory::next<'a>);
impl<'a> Future for FactoryNextShim<'a> {
typeOutput = Option<dynx Debug>;
fnnext(self: Pin<&mutSelf>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let ret = <Factory::next<'a> as Future>::poll(
// This is always sound, probably
pin_project!(self).0,
cx,
);
match ret {
Poll::Ready(Some(output)) => {
// We would skip boxing for #[dyn(identity)] on// impl Future for Factory::next.. which means// #[dyn(identity)] on the impl async fn?// Or do we provide a way to annotate the// future and `impl Debug` separately? TODO//// Why Box::new and not Box::pin like below?// Because `Debug` has no `self: Pin` methods.let boxed = Box::new(output);
Poll::Ready(<dynx Debug>::new(boxed))
}
Poll::Ready(None) | Poll::Pending => {
// No occurrences of `Output` in these variants.
Poll::Pending
}
}
}
}
let wrapped = FactoryNextShim(fut);
// We would skip boxing for #[dyn(identity)]// Why Box::pin? Because `Future` has a `self: Pin` method.let boxed = Box::pin(wrapped);
<dynx Future>::new(boxed)
}
}
This looks to be a lot of code, but here's what it boils down to:
For some impl Foo<A = impl Bar>,
We generate a wrapper type of our outer impl Foo and implement the Foo trait on it. Our implementation forwards the methods to the actual type, takes the return value, and maps any occurrence of the associated type A in the return type to dynx Bar.
This mapping can be done structurally on the return type, and it benefits from all the flexibility of dynx that we saw before. That means it works for references to A, provided the lifetime bounds on the trait's impl Bar allow for this.
There are probably cases that can't work. We should think more about what those are.