Weird error with tokio::try_join! and mapped futures

Yes, well... I would say, using GATs. Or GATs and TAITs; if you can't name your outputs, you need both.

I've hit this error myself and thought "hmmm really?", but have never really followed up on it. You can read RFC 447 for the motivations; the case of an unconstrained associated type is explicitly called out as a drawback. But I can see how it would be problematic: under RFC 447, if we have a concrete type that implements a concrete trait, there is only one possible implementation, and thus all associated types and other non-parameterized trait items are also concrete. Allowing this case would break that tautology, which is used a lot.

trait Tr<X> { type D: Default; }
fn _f<T: Tr<()>>(_: T) {
    // If you can call this function, `T::D` is a concrete type
    let _: T::D = Default::default();
    // More generally,
    let _: <SomeType<A, B> as SomeTrait<C, D, E>>::F = ...;
    // If the type and all its inputs are known,
    // and the trait and all its inputs are known,
    // then then impl and all its inputs are also known,
    // and thus its items (like associated types) are also known
}

Anyway, yes, there is exactly one (plain) associated type per implementation. That's true independent of whether the type uses TAIT or not.

In a more general sense, for any $Thing, there's exactly one definition for every set of concrete input parameters that satisfy their bounds. You can look at (plain) associated types as:

  • Having no input parameters, within the context of a concrete implementation
    • And thus there's only one in this context
  • Having the same input parameters as the implementation, within a broader context
    • E.g. <Vec<String> as Deref>::Target

If I understand what you mean by quantification -- ways to make multiple? -- then there must be a parameter somewhere, and that parameter must be "in scope" to be part of the definiton (be it explicitly on the right, or implicitly in the defining use). As I understand it, in fact, impl Trait captures all parameters in scope (which is of practical importance if the parameters happen to carry lifetimes).

Outside a trait, that can be a parameter on a generic function:

  • fn foo<X>() -> impl Trait ...

Within a trait, that parameter might be:

  • On a GAT: type Gat<X> = impl Trait ...
  • On the trait itself (when impl Trait is on either a GAT or a plain associated type)
  • But return-position impl Trait is still not allowed...

So if there's going to be more than one definition per implementation, that means a GAT must have supplied the parameter. And to emulate return-position impl Trait, if you want the function parameters to be in scope, you have to weave them through by adding parameters to a GAT as well.

  • :x: fn method<T>(&self) -> impl Trait;
  • :white_check_mark: fn method<T>(&self) -> Self::Gat<T>; type Gat<T>: impl Trait;

(I suspect there will be sugar for this some day, but who knows when.)

1 Like