That's the conclusion I came to after sleeping on it.
Dynamic dispatch already requires specifying bounds for associated types; if I use a regular trait, the compiler complains otherwise:
fn dyn_use(iterator: &mut Iterator) {
for item in iterator {}
}
The same would apply to the original example, from the blog post:
fn use_dyn(di: &mut dyn AsyncIterator) {
di.next().await; // <— this call right here!
}
Needs to be rewritten as:
fn use_dyn(di: &mut dyn AsyncIterator<Item = ??>) {
di.next().await; // <— this call right here!
}
That is, use_dyn
is runtime polymorphic about the particular implementation of the trait, but still requires compile-time information about a number of its properties. A single dyn_use
cannot, at runtime, handle a mix of i32
and String
.
Do we really want to try and have use_dyn
be runtime polymorphic over the various future types that various implementation of AsyncIterator<Item = X>
could have?
This seems at odd with the fact that Item
needs to be pinned to a specific type.
And if that requirement is lifted, then suddenly things fall in place (with a hypothetical FutureType
associated item):
fn use_dyn(
di: &mut dyn AsyncIterator<Item = X, FutureType = Box<dyn Future<Output = Option<X>>>>
) {
di.next().await; // <— this call right here!
}
It can be made much more palatable with some library goodness:
trait DynAsyncIterator {
type Item;
type SizedFuture<T: ?Sized>: Future<Output = Option<Self::Item>>;
fn poll_next(&mut self) -> SizedFuture<dyn Future<Output = Option<Self::Item>>>;
}
// [std] only, not [core].
// Make part of prelude, for seamless experience.
trait AsyncIteratorExt: AsyncIterator {
type Boxed: DynAsyncIterator<Item = Self::Item, SizedFuture = Box>;
// Similar to the "fused" adaptor existing on Iterator.
fn boxed(self) -> Self::Boxed;
}
impl<T: AsyncIterator> AsyncIteratorExt for T {
type Boxed = /* todo */;
fn boxed(self) -> Self::Boxed { todo!() }
}
Which enables a nifty:
fn make_dyn<AI: AsyncIterator>(ai: AI) {
// Explicit choice of strategy.
use_dyn(&mut ai.boxed());
}
fn use_dyn(di: &mut dyn DynAsyncIterator<Item = X, SizedFuture = Box>) {
di.next().await;
}
It looks pretty good, if I say so myself:
- Designing
DynAsyncIterator
is a one-off cost for the designer, and a one-off cost for each implementation of which there'll probably be relatively few -- there's not that many strategies available. AsyncIteratorExt
is a one-off cost for the designer.- Even if talking about the return type of
poll_next
was possible,DynAsyncIterator
is more user-friendly than having to specified the horrendously long type. - No unstable compiler features were harmed in this sample -- with GATs stabilized -- though
AsyncIterator
may still requireimpl Trait
in return position in traits by itself, of course. - Compatible with
[no_std]
: theboxed
adapter is purely optional, leaving instd
itself. Users in[no_std]
will instead pick a different strategy, possibly aBox<T, InlineStorage<N>>
.
And of course, the usage is fairly need. A simple call to .boxed()
is both succinct enough not to be a bore, and explicit enough to spot allocations.