Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tracking issue for specialization (RFC 1210) #31844

Open
nikomatsakis opened this Issue Feb 23, 2016 · 146 comments

Comments

Projects
None yet
@nikomatsakis
Copy link
Contributor

nikomatsakis commented Feb 23, 2016

This is a tracking issue for specialization (rust-lang/rfcs#1210).

Major implementation steps:

  • Land #30652 =)
  • Restrictions around lifetime dispatch (currently a soundness hole)
  • default impl (#37653)
  • Integration with associated consts
  • Bounds not always properly enforced (#33017)
  • Should we permit empty impls if parent has no default members? #48444
  • implement "always applicable" impls #48538

Unresolved questions from the RFC:

  • Should associated type be specializable at all?
  • When should projection reveal a default type? Never during typeck? Or when monomorphic?
  • Should default trait items be considered default (i.e. specializable)?
  • Should we have default impl (where all items are default) or partial impl (where default is opt-in)
  • How should we deal with lifetime dispatchability?
@aturon

This comment has been minimized.

Copy link
Member

aturon commented Feb 24, 2016

Some additional open questions:

  • Should we revisit the orphan rules in the light of specialization? Are there ways to make things more flexible now?
  • Should we extend the "chain rule" in the RFC to something more expressive, like the so-called "lattice rule"?
  • Related to both of the above, how does negative reasoning fit into the story? Can we recover the negative reasoning we need by a clever enough use of specialization/orphan rules, or should we make it more first-class?
@arielb1

This comment has been minimized.

Copy link
Contributor

arielb1 commented Feb 24, 2016

I am not sure that specialization changes the orphan rules:

  • The "linking" orphan rules must stay the same, because otherwise you would not have safe linking.
  • I don't think the "future compatibility" orphan rules should change. Adding a non-specializable impl under you would still be a breaking change.

Worse than that, the "future compatibility" orphan rules keep cross-crate specialization under pretty heavy control. Without them, default-impls leaving their methods open becomes much worse.

I never liked explicit negative reasoning. I think the total negative reasoning specialization provides is a nice compromise.

@sgrif

This comment has been minimized.

Copy link
Contributor

sgrif commented Mar 20, 2016

Should this impl be allowed with specialization as implemented? Or am I missing something?
http://is.gd/3Ul0pe

@sgrif

This comment has been minimized.

Copy link
Contributor

sgrif commented Mar 20, 2016

Same with this one, would have expected it to compile: http://is.gd/RyFIEl

@sgrif

This comment has been minimized.

Copy link
Contributor

sgrif commented Mar 20, 2016

Looks like there's some quirks in determining overlap when associated types are involved. This compiles: http://is.gd/JBPzIX, while this effectively identical code doesn't: http://is.gd/0ksLPX

@SergioBenitez

This comment has been minimized.

Copy link
Contributor

SergioBenitez commented Mar 23, 2016

Here's a piece of code I expected to compile with specialization:

http://is.gd/3BNbfK

#![feature(specialization)]

use std::str::FromStr;

struct Error;

trait Simple<'a> {
    fn do_something(s: &'a str) -> Result<Self, Error>;
}

impl<'a> Simple<'a> for &'a str {
     fn do_something(s: &'a str) -> Result<Self, Error> {
        Ok(s)
    }
}

impl<'a, T: FromStr> Simple<'a> for T {
    fn do_something(s: &'a str) -> Result<Self, Error> {
        T::from_str(s).map_err(|_| Error)
    }
}

fn main() {
    // Do nothing. Just type check.
}

Compilation fails with the compiler citing implementation conflicts. Note that &str doesn't implement FromStr, so there shouldn't be a conflict.

SergioBenitez added a commit to SergioBenitez/Rocket that referenced this issue Mar 23, 2016

Now support Result responses.
Experimented with the new impl specialization features of Rust. They work! But
they're not quite there yet. Specifically, I was able to specialize on
`Responder`, but when trying to remove the macro in `FromParam`, it didn't work.
See rust-lang/rust#31844.
@aturon

This comment has been minimized.

Copy link
Member

aturon commented Mar 23, 2016

@sgrif

I had time to look at the first two examples. Here are my notes.

Example 1

First case, you have:

  • FromSqlRow<ST, DB> for T where T: FromSql<ST, DB>
  • FromSqlRow<(ST, SU), DB> for (T, U) where T: FromSqlRow<ST, DB>, U: FromSqlRow<SU, DB>,

The problem is that these impls overlap but neither is more specific than the other:

  • You can potentially have a T: FromSql<ST, DB> where T is not a pair (so it matches the first impl but not the second).
  • You can potentially have a (T, U) where:
    • T: FromSqlRow<ST, DB>,
    • U: FromSqlRow<SU, DB>, but not
    • (T, U): FromSql<(ST, SU), DB>
    • (so the second impl matches, but not the first)
  • The two impls overlap because you can have a (T, U) such that:
    • T: FromSqlRow<ST, DB>
    • U: FromSqlRow<SU, DB>
    • (T, U): FromSql<(ST, SU), DB>

This is the kind of situation that lattice impls would allow -- you'd have to write a third impl for the overlapping case, and say what it should do. Alternatively, negative trait impls might give you a way to rule out overlap or otherwise tweak which matches are possible.

Example 2

You have:

  • Queryable<ST, DB> for T where T: FromSqlRow<ST, DB>
  • Queryable<Nullable<ST>, DB> for Option<T> where T: Queryable<ST, DB>

These overlap because you can have Option<T> where:

  • T: Queryable<ST, DB>
  • Option<T>: FromSqlRow<Nullable<ST>, DB>

But neither impl is more specific:

  • You can have a T such that T: FromSqlRow<ST, DB> but T is not an Option<U> (matches first impl but not second)
  • You can have an Option<T> such that T: Queryable<ST, DB> but not Option<T>: FromSqlRow<Nullable<ST>, DB>
@aturon

This comment has been minimized.

Copy link
Member

aturon commented Mar 23, 2016

@SergioBenitez

Compilation fails with the compiler citing implementation conflicts. Note that &str doesn't implement FromStr, so there shouldn't be a conflict.

The problem is that the compiler is conservatively assuming that &str might come to implement FromStr in the future. That may seem silly for this example, but in general, we add new impls all the time, and we want to protect downstream code from breaking when we add those impls.

This is a conservative choice, and is something we might want to relax over time. You can get the background here:

@sgrif

This comment has been minimized.

Copy link
Contributor

sgrif commented Mar 23, 2016

Thank you for clarifying those two cases. It makes complete sense now

On Tue, Mar 22, 2016, 6:34 PM Aaron Turon notifications@github.com wrote:

@SergioBenitez https://github.com/SergioBenitez

Compilation fails with the compiler citing implementation conflicts. Note
that &str doesn't implement FromStr, so there shouldn't be a conflict.

The problem is that the compiler is conservatively assuming that &str
might come to implement FromStr in the future. That may seem silly for
this example, but in general, we add new impls all the time, and we want to
protect downstream code from breaking when we add those impls.

This is a conservative choice, and is something we might want to relax
over time. You can get the background here:

http://smallcultfollowing.com/babysteps/blog/2015/01/14/little-orphan-impls/


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
#31844 (comment)

@SergioBenitez

This comment has been minimized.

Copy link
Contributor

SergioBenitez commented Mar 23, 2016

@aturon

The problem is that the compiler is conservatively assuming that &str might come to implement FromStr in the future. That may seem silly for this example, but in general, we add new impls all the time, and we want to protect downstream code from breaking when we add those impls.

Isn't this exactly what specialization is trying to address? With specialization, I would expect that even if an implementation of FromStr for &str were added in the future, the direct implementation of the Simple trait for &str would take precedence.

@sgrif

This comment has been minimized.

Copy link
Contributor

sgrif commented Mar 23, 2016

@SergioBenitez you need to put default fn in the more general impl. Your
example isn't specializable.

On Tue, Mar 22, 2016, 6:54 PM Sergio Benitez notifications@github.com
wrote:

@aturon https://github.com/aturon

The problem is that the compiler is conservatively assuming that &str
might come to implement FromStr in the future. That may seem silly for this
example, but in general, we add new impls all the time, and we want to
protect downstream code from breaking when we add those impls.

Isn't this exactly what specialization is trying to address? With
specialization, I would expect that even if an implementation of FromStr
for &str were added in the future, the direct implementation for the
trait for &str would take precedence.


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
#31844 (comment)

@burdges

This comment has been minimized.

Copy link

burdges commented Apr 1, 2016

I think "default" trait items being automatically considered default sounds confusing. You might want both parametricity for a trait like in Haskell, etc. along side with easing the impls. Also you cannot easily grep for them like you can for default. It's not hard to both type the default keyword and give a default implementation, but they cannot be separated as is. Also, if one wants to clarify the language, then these "default" trait items could be renamed to "trait proposed" items in documentation.

@Stebalien

This comment has been minimized.

Copy link
Contributor

Stebalien commented Apr 15, 2016

Note from #32999 (comment): if we do go with the lattice rule (or allow negative constraints), the "use an intermediate trait" trick to prevent further specialization of something will no longer work.

@arielb1

This comment has been minimized.

Copy link
Contributor

arielb1 commented Apr 15, 2016

@Stebalien

Why won't it work? The trick limits the specialization to a private trait. You can't specialize the private trait if you can't access it.

@Stebalien

This comment has been minimized.

Copy link
Contributor

Stebalien commented Apr 15, 2016

@arielb1 Ah. Good point. In my case, the trait isn't private.

@arielb1

This comment has been minimized.

Copy link
Contributor

arielb1 commented Apr 15, 2016

I don't think the "externals can't specialize because orphan forward-compatibility + coherence rulea" reasoning is particularly interesting or useful. Especially when we don't commit to our specific coherence rules.

@burdges

This comment has been minimized.

Copy link

burdges commented May 7, 2016

Is there a way to access an overridden default impl? If so, this could aid in constructing tests. See Design By Contract and libhoare.

@rphmeier

This comment has been minimized.

Copy link
Contributor

rphmeier commented May 7, 2016

Allowing projection of default associated types during type-checking will allow enforcing type inequality at compile-time: https://gist.github.com/7c081574958d22f89d434a97b626b1e4

#![feature(specialization)]

pub trait NotSame {}

pub struct True;
pub struct False;

pub trait Sameness {
    type Same;
}

mod internal {
    pub trait PrivSameness {
        type Same;
    }
}

use internal::PrivSameness;

impl<A, B> Sameness for (A, B) {
    type Same = <Self as PrivSameness>::Same;
}

impl<A, B> PrivSameness for (A, B) {
    default type Same = False;
}
impl<A> PrivSameness for (A, A) {
    type Same = True;
}

impl<A, B> NotSame for (A, B) where (A, B): Sameness<Same=False> {}

fn not_same<A, B>() where (A, B): NotSame {}

fn main() {
    // would compile
    not_same::<i32, f32>();

    // would not compile
    // not_same::<i32, i32>();
}

edited per @burdges' comment

@burdges

This comment has been minimized.

Copy link

burdges commented May 7, 2016

Just fyi @rphmeier one should probably avoid is.gd because it does not resolve for Tor users due to using CloudFlare. GitHub works fine with full URLs. And play.rust-lang.org works fine over Tor.

@SimonSapin

This comment has been minimized.

Copy link
Contributor

SimonSapin commented May 7, 2016

@burdges FWIW play.rust-lang.org itself uses is.gd for its "Shorten" button.

It can probably be changed, though: https://github.com/rust-lang/rust-playpen/blob/9777ef59b/static/web.js#L333

@pythonesque

This comment has been minimized.

Copy link
Contributor

pythonesque commented Oct 12, 2018

Can someone more concretely explain the rationale behind not allowing projections for default associated types in fully-monomorphic cases? I have a use case where I would like that functionality (in particular, it would be semantically incorrect for the trait to ever be invoked with types that weren't fully monomorphic), and if there's no soundness issue I don't completely understand why it's disallowed.

@sgrif

This comment has been minimized.

Copy link
Contributor

sgrif commented Oct 16, 2018

@pythonesque There's some discussion at #42411

@pythonesque

This comment has been minimized.

Copy link
Contributor

pythonesque commented Oct 17, 2018

Ah, I understand if it turns out that projection interacts badly with specialization in general. . And it is indeed true that what I want is of a "negative reasoning" flavor (though closed traits would not really be sufficient).

Unfortunately, I'm not sure if there's really any way to do what I want without such a feature: I'd like to have an associated type that outputs "True" when two passed-in types implementing a particular trait are syntactically equal, and "False" when they aren't (with the "False" case triggering a more expensive trait search which can decide whether they are "semantically" equal). The only real alternative seems (to me) to be to just always do the expensive search; which is fine in theory, but it can be a lot more expensive.

(I could work around this if the trait were intended to be closed, by just enumerating every possible pair of constructors in the head position and having them output True or False; but it's intended to be open to extension outside the repository, so that can't possibly work, especially since implementations in two different user repositories wouldn't necessarily know about each other).

Anyway, maybe this is just an indication that what I want to do is a bad fit for the trait system and I should switch to some other mechanism, like macros :P

@gnzlbg

This comment has been minimized.

Copy link
Contributor

gnzlbg commented Oct 17, 2018

And it is indeed true that what I want is of a "negative reasoning" flavor (though closed traits would not really be sufficient).

An alternative to negative reasoning is requiring that a type implements only one trait of a closed set of traits, such that implementations with other other traits in the set cannot overlap (e.g. T implements one of { Float | Int | Bool | Ptr }).

@pythonesque

This comment has been minimized.

Copy link
Contributor

pythonesque commented Oct 17, 2018

Even if there were a way to enforce that in Rust (which there isn't, AFAIK?), I do not think that would solve my problem. I would like users in different crates to be able to implement an arbitrary number of new constants, which should compare equal only to themselves and unequal to every other defined constant, including ones unknown at crate definition time. I don't see how any closed set of traits (or even set of families of traits) can accomplish that goal by itself: this is a problem that fundamentally can't be solved without looking directly at the types. The reason it would be workable with default projections is that you could default everything to "don't compare equal" and then implement equality of your new constant to itself in whatever crate you defined the constant in, which wouldn't run afoul of orphan rules because all the types in the trait implementation were in the same crate. If I wanted almost any such rule but equality, even this wouldn't work, but equality is good enough for me :)

@rmanoka

This comment has been minimized.

Copy link

rmanoka commented Oct 26, 2018

On present nightly, this works:

trait Foo {}
trait Bar {}

impl<T: Bar> Foo for T {}
impl Foo for () {}

but even with specialization, and using nightly, this does not:

#![feature(specialization)]

trait Foo<F> {}
trait Bar<F> {}

default impl<F, T: Bar<F>> Foo<F> for T {}
impl<F> Foo<F> for () {}

Does this have a rationale or is it a bug?

@Boscop

This comment has been minimized.

Copy link

Boscop commented Oct 27, 2018

@rmanoka Isn't this just the normal orphan rules? In the first case, no downstream crate could impl Bar for () so the compiler allows this, but in the second example, a downstream crate could impl Bar<CustomType> for () which would conflict with your default impl.

@rmanoka

This comment has been minimized.

Copy link

rmanoka commented Oct 27, 2018

@Boscop In that scenario, the default impl should anyway be overridden by the non-default one below. For instance, if I had: impl Bar<bool> for () {} added before the other impls, then I would expect it to work (as per RFC / expectation). Isn't that correct?

Digging deeper along the lines of the counter-example you've mentioned, I realise (or believe) that the example satisfies the "always-applicable" test, and may be being worked on.

@zserik

This comment has been minimized.

Copy link

zserik commented Dec 31, 2018

This issue probably depends on #45814.

@Gladdy

This comment has been minimized.

Copy link

Gladdy commented Feb 12, 2019

Are there any plans to support trait bounds on the default that are not present in the specialisation?

As an example for which this would be very useful, such that you can easily compose handling of different types by creating a generic Struct with arbitrary Inner for the functionality that shouldn't be shared.

#![feature(specialization)]
trait Handler<M> {
    fn handle(&self, m:M);
}

struct Inner;
impl Handler<f64> for Inner {
    fn handle(&self, m : f64) {
        println!("inner got an f64={}", m);
    }
}

struct Struct<T>(T);
impl<T:Handler<M>, M:std::fmt::Debug> Handler<M> for Struct<T> {
    default fn handle(&self, m : M) {
        println!("got something else: {:?}", m);
        self.0.handle(m)
    }
}
impl<T> Handler<String> for Struct<T> {
    fn handle(&self, m : String) {
        println!("got a string={}", m);
    }
}
impl<T> Handler<u32> for Struct<T> {
    fn handle(&self, m : u32) {
        println!("got a u32={}", m);
    }
}

fn main() {
    let s = Struct(Inner);
    s.handle("hello".to_string());
    s.handle(5.0 as f64);
    s.handle(5 as u32);
}

Furthermore, in the example above, something odd that I've experienced - after removing the trait bound on the default Handler impl (and also self.0.handle(m)) the code compiles without issues. However, when you remove the implementation for u32, it seems to break the other trait deduction:

#![feature(specialization)]
trait Handler<M> {
    fn handle(&self, m:M);
}

struct Struct<T>(T);
impl<T, M:std::fmt::Debug> Handler<M> for Struct<T> {
    default fn handle(&self, m : M) {
        println!("got something else: {:?}", m);
    }
}
impl<T> Handler<String> for Struct<T> {
    fn handle(&self, m : String) {
        println!("got a string={}", m);
    }
}
// impl<T> Handler<u32> for Struct<T> {
//     fn handle(&self, m : u32) {
//         println!("got a u32={}", m);
//     }
// }
fn main() {
    let s = Struct(());
    s.handle("hello".to_string());
    s.handle(5.0 as f64);
}

Even though there is no code calling the handler for u32, the specialisation not being there causes the code to not compile.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.