Specialization, coherence, and API evolution


#1

I just published a blog post about specialization, its relation to coherence, and the path to stabilization. Here’s a snippet:


Specialization has been available in nightly Rust for over a year, and we’ve recently been thinking about the steps needed to stabilize it.

There are a couple of implementation issues that are currently blocked on an overhaul of the trait system which should be coming in the next couple of months.

What I want to talk about, though, is some deeper design questions that need to be resolved prior to stabilization, ones involving potential changes to the core specialization rules. This is a story that begins with a bold hope, runs headlong into a tragic discovery, and ultimately ends up close to where it started.

Read more


#2

Fantastic write-up, thanks.

I’ve been playing a lot with trait based metaprogramming, currently using all of:

#![feature(specialization)]
#![feature(associated_type_defaults)]
#![feature(conservative_impl_trait)]

Your post makes me incredibly optimistic. The trait system is already deceptively powerful, and where I see limitations of specialization I think almost all of them could be addressed by some combination of “intersection impls”, “type structure precedence”, or “child trumps parent”.

TBH, I suspect that most limitations could be addressed reasonably with any single one of them, and getting all three would just increase the ergonomics.

Edit: Oh…and we need https://github.com/rust-lang/rust/issues/20041 :slight_smile:


#3

Great post. One thing I wanted to note, because I think it’s important: in my series of posts on specialization, I was focused on a particular case: adding a blanket impl of Clone for all types that implement Copy. Or, more generally, adding a blanket impl of a supertrait in terms of a subtrait. Some examples:

  • impl<T: Copy> Clone for T
  • impl<T: Ord> PartialOrd for T
  • impl<T: Eq> PartialEq for T // this one doesn’t work because Eq lacks methods :cry:

But then I started to get greedy, and to wonder if we could make it legal to add any new impl (e.g., impl<T: Display> Debug for T), since supporting these kind of “bridge impls” was one of our original goals of specialization. I think what your post shows is that bridge impls are untenable: but implementing a supertrait in terms of a subtrait can still work.

To me this goes back to the zero-sum logic from “rebalancing coherence”: we allow child crates to (implicitly) use negative reasoning today relating to their local types. This implies then that parent crates can’t add an impl of an existing trait that may apply to existing types in a downstream crate, since they may already be relying on this negative reasoning.

But all is not lost. The supertrait case is pretty useful. And it seems like the Display/Debug thing was largely about convenience: we could probably still have a default impl that covered this case (if those were implemented), so that while you do have to opt in to implementing Debug, you don’t have to write out the body.


#4

I haven’t finished reading but just noticed a tiny typo:

The overlap rule, which says that a given trait cannot have to impls…

I’ll get back to it now :smiley:


#5

Since I just ran into it, I thought this concrete example that isn’t yet possible with the state of nightly specialization: https://is.gd/Q0RHs5

See the comment on the impl of Foo.

Basically I want to blanked impl for anything that already is Add, but who’s output parameter is the same as Self.

But I also want to be able to add additional implementations, which is fine for things those don’t implement Add at all, but breaks for things that implement Add with a different Ouput type.

struct NotAddable();

pub trait Foo
{
    type B;
    fn foo(self, b: Self::B) -> Self::B;
}

impl<C> Foo for C
    where C: Add<Output = C>
{
    type B = C;
    fn foo(self, other: Self::B) -> Self::B {
        self + other
    }
}

impl Foo for NotAddable {
    type B = Self;
    fn foo(self, b: Self::B) -> Self::B {
       unimplemented!()
    }
}

impl Foo for String { 
//String is Add,but Output!=C, so should be non-conflicting
    type B = Self;
    fn foo(self, b: Self::B) -> Self::B {
       unimplemented!()
    }
}

#6

There’s an RFC for this. Currently the only negative reasoning that’s done is “type does not impl trait,” but that kind would be safe to add as well. It’s a separate feature from specialization though.


#7

Thanks…as only a consumer of these things, is sometimes hard to conceptualize which part of the trait system I’m knocking up against. I just think “should be able to express this logical relationship” :slight_smile:


#8

I may have missed something, but it loks like String implements Add<&str> rather than Add<String>. Change the where clause to where C: Add<C, Output=C> gives the same error, though; does that mean the (lack of) negative reasoning also extends to not being able to distinguish between Add<T> and Add<U>?


#9

Good point. Rereading this this is an orphan rule issue: the impl isn’t allowed so that std can’t be prevented from adding impl Add<String> for String.