-
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: Implied #[derive(SuperTrait)] #2385
Conversation
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.
I do not think that this is anywhere near important enough that we should be considering breaking changes. Given that this RFC intentionally limits the scope to built-in derives, the compiler should have more than enough information to do this without breaking if a manual impl is present.
|
||
### Custom derive | ||
|
||
+ This RFC does **not** affect the behavior of `#[derive(..)]` for traits |
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 is unfortunate, but does answer the question I was going to ask about how trait resolution comes into play
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.
See also the section "Future work" wrt. #[proc_macro_derive(Sub, implies(Super))]
.
|
||
## Breakage | ||
|
||
This RFC will break some code. Specifically, if a user has already manually |
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.
I do not think that this should be considered acceptable. You've used Clone
as your example, in which case, sure -- That's not realistic breakage that is going to happen. However, it is perfectly reasonable to have a manual impl of PartialEq
, which is represents full equality, and just want to #[derive(Eq)]
. I do not think it is worth breaking the stability guarantees that Rust has promised because it leads to other inconveniences.
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.
However, it is perfectly reasonable to have a manual impl of PartialEq, which is represents full equality, and just want to #[derive(Eq)].
That's exactly what I wanted to say as well.
Since Eq
is just a marker, deriving it is common while manual implementation of PartialEq
is provided.
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.
I think this is a perfectly reasonable objection. However, I think that the scenario you mention will be unlikely, and easily mitigated with just invoking rustfix
and then it becomes #[derive(only(Eq))]
which is also more clear wrt. intent. On balance, I think the net win is significant enough to warrant the change.
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.
I really don't like seeing rustfix
mentioned as an argument for breaking changes. a) It's alpha, we cannot rely on it being ready and working perfectly well with the edition, b) it still introduces aggrevation, and rustfix is only a stopgap.
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.
But it is (at least to me) an argument for edition breaking changes having a less severe impact. How much of a mitigating factor rustfix
should be considered as is of course subjective.
Regarding your points:
a) We have until August? rustfix
will have to be sufficiently well functioning for other changes such as those for modules. Furthermore, the analysis around #[derive(..)]
should not be very complicated because we are dealing with a fixed and very limited set of traits where the transitive closures have low cardinality.
b) I can't quantify aggrevation so I am not sure how I should reason about this.
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 idea of rustfix is to allow people to deal with breakage in an easier way. It is not meant to allow or support breakage in the first place. Rustfix or not, breakage is still bad and should be avoided if possible. Here it is definitely possible and the arguments brought in favour of breakage don't really seem convincing to me.
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.
Additionally, while rustfix
might be able to take over the "change code" phase of a migration, in many if not most cases you will still want or need to review all changes. This becomes more work the more things rustfix
fixes. And at some point there will be situations where you'll want to move a 10 year old code base to current edition, and you'll have to work through the accumulated changes in 3 editions.
|
||
### Mitigating factors | ||
|
||
1. We can do this breakage as part of edition 2018. |
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.
"can" != "should"
I really do not want to see "there's a new edition" be used as an excuse to make breaking changes that would not otherwise be considered. If we go down that path, IMO Rust has failed at its stability promises.
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.
I disagree wrt. breaking stability promises. This, or similar changes wrt. breakage, will not break code written for edition 2015. Only code that opts in to edition 2018 will see any breakage and so no code that worked before will break when upgrading your version of rustc unless you make changes and opt into the new edition.
Further, rustfix
can easily help people migrate semi-automatically without doing any more work than just accepting all the fixes. In particular, if we permit #[derive(only(Foo))]
, then rustfix
can output a very small diff.
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.
I really do not want to see "there's a new edition" be used as an excuse to make breaking changes that would not otherwise be considered.
I'm not sure what you mean by "would not otherwise be considered". This is a change we've wanted to make for a long time -- dating from #1028 -- but we backed off last time because of stability concerns. Part of the point of an edition (not the whole point, but an important point) was to let us revisit questions like this, where the expected impact is low (and can of course be mitigated via lints and the usual transition story). Anyway, I don't like arguing in GH line comments, so I'll leave it at that, but perhaps below I'll try to make the 'positive case' for this change a bit stronger (i.e., why it's valuable from an ergonomics and learnability POV).
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.
But how does your comment from back then not apply anymore?
text/0000-implied-derive.md
Outdated
3. It is expected that the breakage will be relatively small because situations | ||
where `Copy` is derived but `Clone` is implemented is rare. | ||
Furthermore, it `Ord` it could be downright risky to derive `Ord` but | ||
manually implement `PartialEq`. |
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.
Did you mean PartialOrd
?
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.
Yep.
|
||
3. It is expected that the breakage will be relatively small because situations | ||
where `Copy` is derived but `Clone` is implemented is rare. | ||
Furthermore, it `Ord` it could be downright risky to derive `Ord` but |
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.
Why? If I have a struct with some super internal private field which should not be taken into account either for ordering or equality, why is this dangerous?
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.
Implementations of PartialEq, PartialOrd, and Ord must agree with each other. It's easy to accidentally make them disagree by deriving some of the traits and manually implementing others.
https://doc.rust-lang.org/nightly/std/cmp/trait.PartialOrd.html#how-can-i-implement-partialord
It occurs to me that in the case the super internal private field which should not be accounted either for ordering or equality, you will manually impl all the above traits and you should, because the behavior becomes much more clear then.
Also, cc @nikomatsakis on this.
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.
Deriving PartialOrd or Ord (but not both) is risky because they must match or things will go awry; if you (e.g.) implement PartialOrd manually to ignore a field, but you derive Ord, then Ord will not ignore the field. This is why clippy, for example, has a lint against this -- or I think it does? At least there is an issue requesting one rust-lang/rust-clippy#1621 =)
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.
@nikomatsakis, for the orderings, once specialization (rust-lang/rust#31844) works, it would be much better to provide the blanket implementations impl<T: Ord> PartialOrd for T
, impl<T: Eq> PartialEq for T
, impl<T: PartialOrd> PartialEq for T
and the disambiguating impl<T: Eq + PartialOrd> PartialEq for T
. That would automatically produce consistent set by defining just Ord
, PartialOrd
or Eq
as desired while specialization should still just take the explicit implementations where provided (I realize someone might have some other complex blanket impls that may not be clearly specializations, but for the epoch I would consider it worthy as it makes defining them much, much easier).
Similar treatment should be possible for Clone, i.e. impl<T: Copy> Clone for T
for the same effect as this RFC. And of the usually derived set, I don't think anything else would need this treatment. That is, @Centril, I am proposing this (adding blanket definitions once specialization works) as an alternative.
Note: also the non-derived FnOnce
, FnMut
and Fn
would benefit from similar treatment. It would defining function objects by hand actually practical.
However, with the future work outlined in the subsection [rustdoc improvements], | ||
this drawback can be mitigated. | ||
|
||
## Surprising for Haskell developers |
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.
Does this really need to be called out as a drawback?
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.
A drawback however insignificant should be mentioned. I always try to write exhaustive RFCs.
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.
In that case, should we call out Ruby modules being able to include/extend/prepend other modules in the prior art section? There are likely several other languages that deserve consideration here as well
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.
Please link the relevant documentation or create a PR against my branch and I will certainly include it in the prior art.
Haskell was specifically mentioned because my familiarity with it and because our deriving system is based on Haskell.
Thank you for the elaborate review @sgrif. I do think the breakage is an important point you make and I've written my thoughts in replies above.
For now yes, that is the intent. However, my preference would be to extend this as much as possible to custom derive macros in the future. A consistent experience in the ecosystem is important to me. I think you are more familiar with the compiler internals than me, so forgive me, and please point out if I am wrong, but changing the behavior here as you propose would require delaying |
I myself am sometimes annoyed when I have to type The point about not doing breaking changes light heartedly by @sgrif is legitimate though... What about keeping |
@est31 If we'd do that, and we could, I'd name the attribute However; I think |
If you added |
We could have the transitive derives be opt-in with something like |
|
||
2. We can give good error messages and help users to migrate with `rustfix`. | ||
|
||
3. It is expected that the breakage will be relatively small because situations |
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.
Note that servo has code that #[derive(Eq)]
but implements PartialEq
manually.
There were also tons of code which #[derive(Copy)]
but implements Clone
themselves because [T; n]
did not implement Clone
for n > 32
.
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.
Thanks; I'll make notes of this in the RFC text.
Granted; however - the separate concept feels "smaller" from a teachability perspective to me and something that can be more easily relegated to an advanced section. We also have older teaching material on the internet to consider.
I'd like to highlight two separate concepts here which often get intermingled in discussion to a single one:
With these definitions of the concepts, it seems to me that
I think this is the hard decision we face in this RFC; I think many will agree that the transitive behavior is what they want, but the breakage is not enticing. It is certainly a judgement call, and I was also initially skeptical. What tipped the scale for me was mainly the ease with which you can
In isolation this is fine; but it does unfortunately not scale: #[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
#[derive(transitive Copy, transitive Ord, Debug, Hash)]
#[derive_transitive(Copy, Ord, Debug, Hash)]
#[derive(Copy, Ord, Debug, Hash)]
pub enum Option<T> { None, Some(T), } between In any case, and for fairness, I will write down |
I agree this sounds unimportant, so not worth any headaches. If this happens, then I'd think
Imho, we should explore more aggressive usage of associated constants and const generics before encouraging trait forests like PartialEq, etc., ala
You might want |
text/0000-implied-derive.md
Outdated
reach from `x` in one or more steps. | ||
|
||
- **super trait** - For a trait `Copy`, defined as `trait Copy : Clone {}`, | ||
the trait `Clone` is a super trait of `Clone`. We also say that if `T: Copy`, |
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.
Should one of the Clone
s be a Copy
?
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.
Indeed it should, thanks.
I'm not sure that we want or need a fully general solution, as opposed to tweaks targeted specifically at "the standard traits". How often does this pattern arise in crates other than In the past there've been some comments about the type safety of PartialEq/Ord not being worth their ergonomic cost; did that ever go anywhere? Maybe we could make this behavior a per-trait opt-in thing. |
I assume you meant I do think however that There's also the problem that we can't guarantee that custom derive macros would honor the transitive closure property of
That does seem neat, but I'm not sure such a large change can be done now. It seems a bit too late for such a design. Tho one should consider such hierarchies for new traits in the future.
This RFC proscribes tweaks to the libstd traits tho. The RFC does not propose a fully general solution in reality. However, the Future work section does hint at possible roads ahead for per-macro opt-in generality.
Consider a hierarchy such as with monoids, semigroups, etc. It would be, at least to me, quite desirable to derive all super traits implicitly when you derive a trait up the hierarchy.
Not sure what this is referring to. Links?
This would also be an edition-break. However, to me, the opt-in behavior is a property of the deriving macro, not of the trait, and as such, the proc macro should be annotated, as hinted at in the Future work section, and not the trait. |
rust-lang/rust-roadmap-2017#17 (comment) Come to think of it, some of the discussion on that issue is basically about the mechanism this RFC proposes.
Wow, somehow I completely missed that part of the Future work section. So, my poorly-expressed intent was that the supertrait would get auto-derived only if no manual impl existed (I think someone suggested that in an earlier comment but I can't see it now), which I assumed would be a slightly magical behavior only feasible for
Although this is a massive tangent I don't want to get too deep into, I'm very skeptical that higher-order categorical traits like these (what is the proper term?) are particularly valuable in a language that's not deeply pure and immutable the way Haskell is, especially in Rust since we can't even write the "building block" higher-order functions like |
Right; so @aturon's point there on the papercut of
I refer you to @sgrif's review here and my reply, here.
Although I don't agree with you on the usefulness of algebra traits, they were just an example -- where there are trait hierarchies, which can be entirely domain specific, transitive deriving reduces boilerplate and improves ergonomics. |
Breaking changes are absolutely toxic for teaching material. It is one thing if the code you paste is giving you a warning or slightly less idiomatic, but a completely different thing if it doesn't even compile. Another teachability downside of your proposal is the inconsistency with the library situation:
You have summarized this really well 👍
I feel that such a change would be quite spammy... It would be better if rustfix only changed the It making breaking changes easy should not be a reason to make breaking changes light heartedly. I don't want to be required to use rustfix, and when I use it I want to have small diffs, I don't want every single line in my program to change.
What about |
I think the compiler could 'just' say:
Alternately, you can get 99% of the way there after only performing name resolution, by looking for impls of the syntactic form, e.g.,
The autogenerated impl can't conflict with any valid impl that's not of this form – the only possibility would be a blanket impl of There might be a question about impls of the form
where
…you have to determine whether the parameter to
…where I don't know much about compiler internals, so I have no idea whether this would actually be hard to determine. But even if it were, it's also probably not a big deal if the 2018 edition broke code using this very specific weird pattern. |
I must unfortunately absolutely 👍 with you here.
Not sure I understand -- what is the "library situation"? Does it refer to custom derive macros?
Certainly
Ideally, in a lot of cases, it would / should be as easy as: <change the edition + version manually>
$ rustfix && cargo test &&
git add . && git commit -m "I did rustfix" && git push &&
cargo publish
That's not so bad 👍
My initial reaction to this is that it would be an ingenious construction.
This is pretty interesting idea!
Is that not desirable tho for a derived impl to use
Indeed it is quite weird. I'd say that the assertion that this would break approximately 0 code would not be bold, and probably true. |
With regard to |
or maybe |
Annotating transitivity for built-in derives begs the question of how you'd do that for proc macros, which already can transitively derive things without having to annotate it. So, I'd rather allow it for builtins without an annotation. |
The point is that the traits themselves are not worth it. That has very little to do with the derive. The actual item on there is:
The ergonomic hit comes from more than just the additional derives. |
+1 to the problem and proposed syntax This: #[derive(Copy)]
struct Foo;
impl Clone for Foo … should continue to work with this change. I'd expect the implied So instead of
|
What would it take to get blanket impls for this stuff working? I'd love to just have bikeshed impl<T: Copy> Clone for T {
fn clone(&self) -> Self { *self }
}
bikeshed impl<T: Ord> PartialOrd for T {
fn partial_cmp(&self, other: &Rhs) -> Option<Ordering> { Some(self.cmp(other)) }
}
// etc in (Edit: |
Summarizing discussions with @scottmcm on IRC: If With respect to specialization and the always applicable rule, the above impl would iirc translate into the following chalk query: forall<A> {
if (WellFormed(A)) {
exists<T> {
T = A,
T: Copy
}
}
} However, it does not flow from |
It would have the huge advantage of actually working for the manual implementations too. And it's even more important then, because then you can't use the other derives now even if the other implementations are trivial. |
Thank you, boats, for that helpful re-framing of the discussion. It makes me, overall, see this discussion as a mirror to a bunch of the discussions in C++ land about Concepts design. For example,
I wonder if a better answer than supertraits is defining a Rust version of what Fundamentals of Generic Programming (Dehnert and Stepanov, '98) calls a Regular Type. We're already doing better than C++ here—we don't need assignment (well, so long as we don't have That is, redefine the problem from subtrait/supertrait to needing to know the list of traits at all. 1st Draft: The thought process for that list is basically "anything where there's not really any interesting choices or downsides for a boring type, so the derived one is what you want and not a future hazard". ( Digression: The careful reader may have noticed that I didn't mention |
I would like to propose a way to use RFC 2010 (trait impl specialization again) to address this, including the If I get it right, a universal default impl should be doable with RFC 2010 for Consider adding For this particular case of multiple This still needs a new mechanism in the compiler, but I would see it as more natural, understandable and general (even though we would likely keep it internal). Also I would hope it would be relatively easy to implement, filtering the candidate method list for this attribute on resolution. Perhaps we already have it in some form? I would be happy to draft an RFC for the attribute if you think it is a good idea, and the current RFC could then be updated to use that and default impls (with specialization). If that works out, would that not resolve the issue entirely? By that I mean that both manual and derived impls of either just the subtraits ( |
FWIW I discussed this a bit with eddyb and it is technically feasible to have such a feature imply a "weak" impl that can be overridden by an explicit impl. There are some hairy issues around generics, but it's not too bad. |
Weak impls are like all kind of overloading absolutely toxic to maintainability of code. you'll never know whether somewhere in some module of the crate, an impl is hiding that overrides the weak impl you are currently trying to read or trying to change. As for compile times it is probably not helpful if the compiler has to process a ton of code only to find out that it is dead. If it can perform that check during macro expansion, we'd save some cycles. As a positive point, weak impls move this RFC out of the breaking change category, but |
The problems this RFC is trying to solve are "you have to type more" and "you have to know a quirk about derives". #[derive(Clone, Copy)] vs #[derive_transitive(Copy)] And it doesn't reduce mental burden, just changes one thing you need to know into another thing you need to know (instead of knowing that for |
An alternative to a new derive attribute like To write out |
Now you'll need to know which traits will be derived recursively and which won't... both during reading AND during writing. And yes you'll still need to know that there are two kinds of derive macros, ones which output implementations of the supertraits and others which only output the given trait precisely. As for the "less typing" argument, you will the more traits you are deriving recursively, the more you will save and after a given number of traits, you'll break even. |
There's an issue about That reminds me of this issue which for the (suggested) implied behavior would need delay generation of an implementation until it's certain that there isn't a manual implementation. So perhaps derive needs some magic behavior anyway? That |
@kornelski Those things are fixed by #2353. |
Oh no, more versions of derive :( |
@kornelski well; not more versions, but modifiers on the deriving mechanism we already have. |
I'm worried that this RFC plus the other RFC will cause a new "Which kind of Derive do you need?" chapter to be added to Rust teaching materials. Instead of:
I'd really like:
I get that the latter is hard to implement and runs into chicken-egg problems, but from user perspective it's so much nicer. |
@Manishearth Thanks for the discussion on the actual feasibility of the weak impl - I was not sure there myself. I am glad to see that this could work. @est31 I see your concern, but if you are against weak impls and specialization in general, that should be discussed in the other RFC. I propose to add few very carefully selected impls from stronger traits to their weaker counterparts with the same obvious semantics (so @kornelski I would hope for the same: a |
At this point, it's clear that this proposal is not viable for the 2018 Edition; there's just too much else in flight, we haven't reached a clear consensus here, and there are plausible routes to addressing the motivation without tying it to the edition. Thus, I'm moving to postpone further discussion on this topic: @rfcbot fcp postpone |
Team member @aturon has proposed to postpone this. The next step is review by the rest of the tagged teams: No concerns currently listed. Once a majority of reviewers approve (and none object), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
I fully support the decision to postpone! To continue the conversation re. solving the problem in a different way, there are some interesting other designs to consider that don't require breaking changes such as ideas mentioned in |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
The final comment period, with a disposition to postpone, as per the review above, is now complete. By the power vested in me by Rust, I hereby postpone this RFC. |
🖼️ Rendered
📝 Summary
When deriving standard library such as
Copy
, the transitive closure of all super traits will also be implicitly derived. That is, it is sufficient to#[derive(Copy)]
to get theClone
impl as well.💖 Thanks
To @aturon, @nikomatsakis, and discussions at Rust All Hands for the idea. See the internals thread for some discussion.