Why doesn't this blanket impl allow trait object coercion?

Hello!

The recent stabilization of trait upcasting had me wondering - why does the following not compile?

trait Param {
    type Domain: Domain;
}

trait Domain {}

trait Foo {
    type Domain: Domain;
    
    fn test(&self, param: &dyn Param<Domain = Self::Domain>);
}

struct TestDomain;

impl Domain for TestDomain {}

struct Test;

impl Foo for Test {
    type Domain = TestDomain;
    
    fn test(&self, param: &dyn Param<Domain = Self::Domain>) {
        external(param);
    }
}

impl<T> Param for T where T: External {
    type Domain = TestDomain;
}

trait External {}

fn external(_param: &dyn External) {
    unimplemented!()
}

It fails with the error that the call to external expects trait External, but found trait Param<Domain = TestDomain>.

However, shouldn't the existence of this covering implementation:

impl<T> Param for T where T: External {
    type Domain = TestDomain;
}

give the compiler enough information to deduce that any T: Param<Domain = TestDomain> also means T: External? And thus, &dyn Param<Domain = TestDomain> should be able to be coerced to &dyn External?

Is this just not implemented as of now? Or maybe I'm missing something?

First, that blanket implementation means that T: External + Sized implies T: Param<Domain = TestDomain, not the other way around.

But changing it to this won't help...

impl<T: ?Sized> External for T where T: Param<Domain = TestDomain> {
}

...because trait upcasting is only for the use case of explicit sub/supertraits (as expressed by the bounds in the trait definition), and is not based on what implementations exist.

And and can't coerce dyn Param<..> itself to dyn External without that relationship[1] because coercions to dyn _ can only happen from another unsized type when you're dropping auto-traits, dropping the principal trait, coercing the dyn lifetime, or -- when the feature lands -- upcasting.

There's more information in the RFC.


  1. e.g. if you simply impl External for dyn Param<Domain = TestDomain> + '_ ↩︎

First, that blanket implementation means that T: External + Sized implies T: Param<Domain = TestDomain, not the other way around.

Right, thank you. I wrote that backwards.

I guess then, in the context of upcasting, what could help here is conditional trait bounds based on a set of discrete types for any associated types.

something like

trait Param: External when Self::Domain = TestDomain {
  type Domain: Domain
}

Basically to allow upcasting to multiple traits without needing to specify them as supertraits for all implementations of Param.