Behavior of Specialization with associated types

The following code compiles as expected:

pub trait Serialize {}

pub trait Unwrapper {
    type Inner: Serialize;

    fn unwrap(&self) -> &Self::Inner;
}

impl<T> Unwrapper for T
where
    T: Serialize,
{
    type Inner = T;
    fn unwrap(&self) -> &Self::Inner {
        self
    }
}

but attempting to use the specialization feature on nightly:

#![feature(specialization)]

pub trait Serialize {}

pub trait Unwrapper {
    type Inner: Serialize;

    fn unwrap(&self) -> &Self::Inner;
}

default impl<T> Unwrapper for T
where
    T: Serialize,
{
    type Inner = T;
    fn unwrap(&self) -> &Self::Inner {
        self  // expected associated type, found type parameter `T`
    }
}

produce the following error:

error[E0308]: mismatched types
  --> src/main.rs:17:9
   |
17 |         self
   |         ^^^^ expected associated type, found type parameter `T`
   |
   = note: expected reference `&<T as Unwrapper>::Inner`
              found reference `&T`
   = note: you might be missing a type parameter or trait bound

Is it a limitation of specialization?

If not what is the reason for this behavior?

This question originates from a SO post

Yes, this is a limitation of specialization, what should happen if I added the impl

// Foo: Serialize
impl Unwrapper for Foo {
    type Inner = Bar;
}
3 Likes

Specialization is made to specialize on implementations: the API is not allowed to change.

In you example you may circumvent the issue making the Unwrapper trait take a <Inner> generic type rather than an associated type, and you even shouldn't then be needing specialization anymore.

1 Like

Many Thanks for the clarifications.

With the default keyword introduced by specialization for the above code example
<T as Unwrapper>::Inner no longer normalizes to <T> and this generate the error.

For what I understand specialization practically prevents
to use associated type as return values of trait methods.

Am I wrong?

default impl does not really mean what one may think. You may be surprised to know that:

#![feature(specialization)]
default impl<T : ?Sized> Foo for T {}
trait Foo {}

does not make all the types be Foo: Playground

Instead, default impl is there to provide default items / to make implementors of the trait be allowed to skip some definitions.

The best example where that is useful is when dealing with trait objects while offering ergonomics:

Click to show an example

Imagine having:

trait Rng
where
    Self : 'static, // let's not bring non-`'static` lifetimes to this example πŸ˜…
{
    fn next (&mut self) -> f64;
}

that you want to use as a trait object (e.g., to be able to dynamically override the rng generation), and yet you also want to be able to "clone" at some point for reproductible rng runs (e.g., for testing): &dyn Rng -> Box<dyn Rng>:

trait ClonableRng
where
    Self : 'static, // let's not bring non-`'static` lifetimes to this example πŸ˜…
{
    fn next (&mut self) -> f64;

    fn clone_boxed (self: &'_ Self) -> Box<dyn ClonableRng>
    {
        // Wops, this requires `Self : Sized + Clone`
        Box::new(self.clone())
    }

    // what about...

    fn clone_boxed (self: &'_ Self) -> Box<dyn ClonableRng>
    where
        // Wops, this is now unusable by a trait object
        Self : Sized + Clone,
    {
        Box::new(self.clone())
    }
}

The only solution is to provide Box::new(slef.clone()) in the context of self not being a trait object, i.e., when impl-ementing the trait for a concrete (or at least Sized + Clone) type!

trait ClonableRng
where
    Self : 'static, // let's not bring non-`'static` lifetimes to this example πŸ˜…
{
    fn next (&mut self) -> f64;

    fn clone_boxed (self: &'_ Self) -> Box<dyn ClonableRng>
    ; // no default impl
}

// Dowstream user:
impl Clone for MySuperDuperType { ... } // or #[derive(Clone)]
impl ClonableRng for MySuperDuperType {
    fn next (&mut self) -> f64;

    // damn, having to provide this for each implementor is
    // distracting, repetitive, and thus annoying!
    fn clone_boxed (self: &'_ Self) -> Box<dyn ClonableRng>
    {
        Box::new(self.clone())
    }
}

to solve this in stable Rust one needs to go through some extra hoops (mainly an extra trait):

trait ClonableRng : CloneDynClonableRng
where
    Self : 'static, // let's not bring non-`'static` lifetimes to this example πŸ˜…
{
    fn next (&mut self) -> f64;
}
// where
trait CloneDynClonableRng {
    fn clone_boxed (self: &'_ Self) -> Box<dyn ClonableRng>
    ; // no default impl here
}

// Default generic impl with the correct bounds! 
impl<T : Clone> CloneDynClonableRng for T
where
    T : ClonableRng, // Oh yes, we also need this bound
{
    fn clone_boxed (self: &'_ Self) -> Box<dyn ClonableRng>
    {
        Box::new(
            self.clone() // Thanks to Clone
        ) // coercion to dyn ClonableRng thanks to the ClonabelRng bound
    }
}

Needless, to say, this leads to a less readable API (extra trait), and not everyone knows this workaround.

This is where the default impl shines! Indeed, you get to add bounds for a default impl without restricting the "official" API of the method (e.g., to keep it being object-safe):

#![feature(specialization)]

trait ClonableRng
where
    Self : 'static, // let's not bring non-`'static` lifetimes to this example πŸ˜…
{
    fn next (&mut self) -> f64;
    fn clone_boxed (self: &'_ Self) -> Box<dyn ClonableRng>
    ; // no default impl here
}

// Default generic impl with the correct bounds! 
default
impl<T : Clone> ClonableRng for T {
    fn clone_boxed (self: &'_ Self) -> Box<dyn ClonableRng>
    {
        Box::new(
            self.clone() // Thanks to Clone
        ) // coercion to dyn ClonableRng thanks to the (implicit!) ClonableRng bound
    }
}

This reads well and is intuitive.

  • As you can see, default impl's purpose is to have partial implementations. It has to be understood as partial impl: the impl only takes place once there is an actual non-default impl that completes it.

What you are looking for is rather something liek:

#![feature(imaginary_syntax)]

pub trait Serialize {}

pub trait Unwrapper {
    type Inner: Serialize;

    fn unwrap(&self) -> &Self::Inner;
}

impl<T> Unwrapper for T
where
    T: Serialize,
{
    default {
        // Here is a default impl, with two items that are tied together:
        type Inner = T;
        fn unwrap(&self) -> &Self::Inner {
            self
        }
    }
}

At first glance it looks like it could work, but the whole semantics of default { ... } blocks would need to be clarified to know if it is really the case.

For instance, the default block would lead to:

fn foo<T : Unwrapper> (it: T) {
    let unwrapped: &T = it.unwrap(); // Error, &T vs &<T as Unwrapper>::Inner
}

which is not a probelm per se, just omething to be aware of.

Now, these default blocks could be skipped altogether thanks to existential types (type_alias_impl_trait):

#![feature(type_alias_impl_trait, specialization)]

pub
trait Serialize {}

pub
trait Unwrapper {
    type Inner: Serialize;

    fn unwrap(&self) -> &Self::Inner;
}

impl<T> Unwrapper for T
where
    T : Serialize + 'static,
{
    // For each `impl` hide the actual type except for the 
    // knowledge of it being `'static` and implementing `Serialize`.
    type Inner = impl 'static + Serialize;

    // Here is one overrideable method that makes `Inner` resolve
    // to `T` before getting abstracted away into `impl Serialize`
    default
    fn unwrap(&self) -> &Self::Inner {
        self
    }
}

struct Wrapper<T>(T);

impl<T> Unwrapper for Wrapper<T>
where
    T : Serialize + 'static,
{
    // For each `impl` hide the actual type except for the 
    // knowledge of it being `'static` and implementing `Serialize`.
    type Inner = impl 'static + Serialize;

    // Here is one overrideable method that makes `Inner` resolve
    // to `T` before getting abstracted away into `impl Serialize`
    fn unwrap(&self) -> &Self::Inner {
        &self.0
    }
}

And by annotating / infecting Unwrapper with a generic lifetime parameter one can get rid of 'static: Playground


And maybe in the future this could be simply written as:

#![feature(imaginary_future)]

pub
trait Unwrapper {
    fn unwrap(&self) -> &impl Serialize;
}

impl<T : Serialize> Unwrapper for T {
    default
    fn unwrap(&self) -> &impl Serialize {
        self
    }
}

struct Wrapper<T>(T);
impl<T : Serialize> Unwrapper for Wrapper<T> {
    fn unwrap(&self) -> &impl Serialize {
        &self.0
    }
}
2 Likes