-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
RFC: impl specialization #1210
RFC: impl specialization #1210
Conversation
} | ||
|
||
partial impl<T: Clone, Rhs> Add<Rhs> for T { | ||
fn add_assign(&mut self, rhs: R) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should have a default
, I think.
How does the compiler handle code such as this: struct Foo<T>;
impl<T> Foo<T> {
default fn test(self) { ... }
}
fn bar(Foo<u32> p) {
p.test();
} It has no way of knowing whether a downstream crate will provide a more specialized implementation of edit: |
I'd imagine downstream crates can't specialize inherent methods of a type, just as they can't add inherent methods to a type today. |
I'm probably gonna come back to this a few times over the next couple days because I feel like there is a lot to chew on here, and even now reading it for the third time I have some things I want to think about more deeply. Overall I like this approach and think that the specified algorithm is a good point in the design space. As I mentioned on IRC I think we should also follow up with a proposal for a detailed implementation strategy, that we (the compilers team, core team, me, any other relevant parties) talk about for a period of time. From my perspective (and I hope others too) it is important that we evaluate our implementation strategy and ensure that it isn't going to cause problems down the road in terms of stability, ICEs, our future compiler design (incremental, parallel, etc). |
This isn't relevant to the current RFC but an alternative explicit ordering mechanism would be a match like syntax: impl<'a, T: ?Sized, U: ?Sized> AsRef<U> for {
&'a T where T: AsRef<U> => {
fn as_ref(&self) -> &T {
<T as AsRef<U>>::as_ref(*self)
}
},
T where T == U => {
fn as_ref(&self) -> &T {
self
}
},
} |
Another alternative [edit] to explicit ordering [/edit] would be to break the overlap using negative bounds. Obviously this is contingent on negative bounds being accepted. This seems like a better approach, since it preserves the intuitive rule used in this proposal along with its various properties. Also, allowing edit: Clarified that I'm talking about an alternative to explicit ordering, not to specialization. |
``` | ||
|
||
This partial impl does *not* mean that `Add` is implemented for all `Clone` | ||
data, but jut that when you do impl `Add` and `Self: Clone`, you can leave off |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/jut/just/
I'm not very comfortable with the idea of introducing a mechanism which can create very tall inheritance trees; if the number of possible Would it be possible therefore to limit the number of I don't think negative bounds and specialization are alternatives to one another at all. This RFC doesn't mention PR #1148 which enables negative bounds without the backcompat hazard that troubled PR #586. I think that these would be complementary changes to the coherence rules which address one another's limitations. Not trying to trumpet my own RFC -- just pointing out that is a related proposal. Haskell has extensions which implement some form of specialization (e.g. OverlappingInstances). It would be a good idea probably to ask in the Haskell community about the pitfalls that implementations of type class specialization have run into. I think this is a situation in which Rust's more OO-influenced heritage makes specialization more useful for us than it was for Haskell though. We'll probably need a new name for Regardless of these, this is an awesome and impressively exhaustive RFC! |
|
||
```rust | ||
impl<T> Debug for T where T: Display { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't that be a default fn fmt(…)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The text hasn't motivated default
at this point. The example is of what the proposal is trying to allow "at the simplest level"—i.e. before the complexity of needing a default
keyword is added—not of what the proposed syntax will ultimately look like once this complexity is taken into account. I thought this was clear, but maybe it could be clarified.
It might be a good idea to limit the appearance of motivating examples which don't follow the proposed syntax. One way would just be to add text saying "ignore the default
keyword for now, it'll be motivated later". This would be unfortunate though, since I liked the style of exposition used here, and adding these caveats would diminish it somewhat. Perhaps a better option is just to add a comment in the example itself saying:
// Note: This example will not work as written under this proposal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the very least, any example that does not actually follow the expected end-syntax could have an explicit annotation saying so, perhaps with a pointer to an end-appendix that shows each such example in the final expected form.
(Also, the "Motivation" section did at least say default
in one example -- so there is at least precedent for using it here as well, if one does not want to go the route of adding an appendix with all of the examples according to their final formulation)
Overall, I'm quite happy with the proposal; I'm a bit worried about corner cases (especially regarding dropck), but I think we should be able to sort them out. Of course, once this RFC is accepted, Rust will cease to even be a language of medium complexity. People will misuse this feature to create a maze of twisty little Therefore the only thing I don't like is the use of the default keyword (despite having it in Java interfaces). I want the keyword to be long, outlandish and hard to remember, so folks will have to think twice before writing it. Something like: |
Requiring that apply(I) and apply(J) are either disjoint or one contained in the other seems excessively restrictive. A less restrictive rule could be this set of equivalent rules:
This allows sets of impls like this:
This would be allowed since the intersection of apply(B) and apply(C) is equal to apply(D), the only apply-set contained in both, and all other apply-set pairs are contained in each other. Not sure about the algorithmic complexity of checking for this though. It appears to be equivalent to SAT solving, but this also applies to checking the exhaustiveness of match patterns, so it's not necessarily an issue. A possible middle ground is to require that the intersection of apply(I) and apply(J) is equal to apply(K) for just one K, which should eliminate the SAT equivalency, and might still be expressive enough. |
Another motivating example is the |
I like it! My only worry is the that the language is getting more complex, but arguable thats not avoidable in this case. Am I right in thinking that this RFC would enable backwards-compatibly solving the partial impl<T> Clone for T where T: Copy {
default fn clone(&self) -> Self {
*self
}
} |
Wow, this basically destroys dropck - we may have to go back to a (maybe compiler-guaranteed) form of No. That requires "lattice impls", which is explicitly not part of this RFC. |
@arielb1 please elaborate how this 'destroys dropck'. |
It completely destroys parametricity, and therefore dropck. I think it may even be a better option to make specialization unsafe for that reason. To see how it destroys parametricity: Suppose you have a completely innocent blanket impl for a impl<'a, T> Clone for &'a T { fn clone(&self) -> Self { *self } } It can be trivially called by a destructor: struct Zook<T>(T);
fn innocent<T>(t: &T) { <&T as Clone>::clone(&t) /* completely innocent, isn't it? */; }
impl<T> Drop for Zook<T> { fn drop(&mut self) { innocent(&self.0); } } However, this can be abused: struct Evil<'a>(&'b OwnsResources /* anything that is invalid after being dropped */);
impl<'a,'b> Clone for &'a Evil<'b> {
fn clone(&self) -> Self {
println!("I can access {} even after it was freed! muhahahaha", self.0);
loop {}
}
}
fn main() {
let (zook, owns_resources);
owns_resources = OwnsResources::allocate();
zook = Zook(Evil(&owns_resources));
} |
Isn't this problem what the |
@nikomatsakis, @pnkfelix and I had discussed the dropck issue a while back (note the brief discussion in Unresolved Questions). There are a few avenues for preventing the interaction you're describing -- note, for example, that in the RFC proposal you need a
The RFC talks a bit about how we could handle this case -- in particular, the overlap/specialization requirements for |
Note that ruling out bad dropck interaction is nontrivial because of examples like the following: trait Marker {}
trait Bad {
fn foo(&self);
}
impl<T: Marker> Bad for T {
default fn foo(&self) {}
}
impl<'a, T> Marker for &'a T {} Here, given an arbitrary UPDATE: the main questions here are: can we convince ourselves that such a restriction retains the needed parametricity for dropck? And does such a restriction still support the main use cases for specialization? This is why I wanted to experiment a bit more before laying out a detailed proposal for the restriction. |
Wouldn't Also, "nontrivial bound": the absence of |
Even more fun from Unsound Labs: this useful-ish and rather innocent code: use std::fmt;
pub struct Zook<T>(T);
impl<T> Drop for Zook<T> { fn drop(&mut self) { log(&self.0); } }
fn log<T>(t: &T) {
let obj = Object(Box::new(t));
println!("dropped object is {:?}", obj);
}
struct Object<T>(Box<T>);
impl<T> fmt::Debug for Object<T> {
default fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "<instance at {:?}>", (&*self.0) as *const T)
}
}
impl<T: fmt::Debug> fmt::Debug for Object<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
self.0.fmt(f)
}
} can be exploited by this wolf in sheep's clothing (not even a single evil structure in sight!): fn main() {
let (zook, data);
data = vec![1,2,3];
zook = Zook(Object(Box::new(&data)));
} enjoy! |
@arielb1 Do you have any idea for how we can provide simple specialization? An example is |
@bluss That sounds like one of the things the optimizer should be able to do reliably. |
@tbu-: it does not (LLVM has no loop idiom recognize for memcmp in this case), and it's one of the example motivations for specialization. |
That should probably also be filed for LLVM, then. |
not a single useful example out there to help us get going with using impl Trait, very sad |
@TitanThinktank this RFC is about specialization. You can find an introduction to Traits (and |
How close from implementation is "lattice" specialization nowadays ? |
I don't think the lattice specialization was ever accepted. So we would need a separate RFC to extend specialization to handle it. But I think that will have to wait until we figure how to deal with the soundness hole in the current implementation of specialization. |
Weren’t the soundness concerns dropped alongside associated type specialization ? That feature is so direly needed one could argue implementing lattice specialization on top or the current work is good enough for now. |
No, the soundness concetns aren't dropped. The minimal version of specialization referred to here is trait Foo { ... }
trait Bar { ... }
impl<T> Foo for T { ... } // defers to Bar
impl<T: Eq> Foo for T { ... }
impl<T> Bar for T { ... }
impl<T, U> Bar for (T, U) { ... } Note: if we get a tuple |
given how rust tends to abbreviate larger keywords, |
Using (Not that the existing abbreviations are perfect either, e.g. |
Could use |
@jvcmarcenes "default" is already everywhere, like Default and Default::default, Option::unwrap_or_default etc. It's a very generic combination of letters that doesn't really sound like default |
Especially useful for you can do:
To implement all existing Generic variants of From and Into. But then you can't implement additional Generic Variants to this:
This results in
But in fact no conflict seems to arise, due to the specialization (as there is no (Or (seeminghly) the problem is introduced by |
To me it seems the 'partial` keyword is not necessary, as there seems to be no danger to brake existing code and it should be obvious to the compiler when multiple implementations for the same Trait with non-overlapping bounds are found. Actual conflicts should be easy to find and reject. |
This RFC proposes a design for specialization, which permits multiple
impl
blocks to apply to the same type/trait, so long as one of the blocks is clearly
"more specific" than the other. The more specific
impl
block is used in a caseof overlap. The design proposed here also supports refining default trait
implementations based on specifics about the types involved.
Altogether, this relatively small extension to the trait system yields benefits
for performance and code reuse, and it lays the groundwork for an "efficient
inheritance" scheme that is largely based on the trait system (described in a
forthcoming companion RFC).
Rendered