-
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
Re-Rebalancing Coherence #2451
Re-Rebalancing Coherence #2451
Conversation
RFC rust-lang#1023 introduced rules that exclude impls which should clearly be valid. It also used some ambiguous language around what is a breaking change, that ended up being completely ignored and contradicted by rust-lang#1105. This RFC seeks to clarify what is or isn't a breaking change when it comes to implementing an existing trait, and conservatively expands the orphan rules to allow impls which do not violate coherence, and fit within the original goals of rust-lang#1023.
Honestly, this RFC is the clearest explanation of the coherence rules I've seen to date - the proposed rules actually seem simpler to understand with less special casing. Is it necessarily true that all blanket impls are a breaking change? At the very least, new traits can obviously have blanket impls backwards compatibly, but even for existing traits, aren't there any cases where a blanket impl can be safely added? I hope in the long term there will be some way for the author of the type/trait to customize the coherence rules (like a more advanced version of the |
I'd go farther than that: This is the only explanation/proposal of the actual coherence rules (as opposed to a pre-rigorous understanding of "why coherence matters") that I ever understood, full stop. Now that I'm looking at it, the formal definition in #1023 is just incomplete and ambiguous (although I assume it was intended to mean something similar to what this new RFC proposes). |
Given `impl<P1...Pn> Trait<T1...Tn> for Target`, an impl is valid only if at | ||
least one of the following is true: | ||
|
||
- `Trait` is a local trait |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
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.
@cramertj This isn't even about the negative reasoning case. It's because this impl is allowed: impl A<LocalStruct> for i32
, which would cause impl<P, T: A<P>> B<P> for T
to conflict with impl<P> B<P> for i32
. We want to continue to allow this for the reasons I've laid out in this RFC. Replace A
with QueryFragment
("compile AST to SQL" in Diesel), LocalStruct
with "database backend", and i32
with "concrete type representing an AST node requiring special syntax for a single database", and hopefully it's more clear why we want to allow that hypothetical impl to exist
With the set of impls that are accepted today, there is no case where a blanket impl can be added for an existing trait with an existing type.
I agree, and I think many others do as well. However, any substantial change to coherence in that direction will require much more work, will almost certainly be completely blocked on chalk, and need to be tied to an edition. My goal with this RFC was for something more conservative that is able to land more quickly, regardless of whether or not we want to develop a more comprehensive solution in the future. |
Just to be clear, these are not meant to be the same |
awesome write up.
I think you should simplify it to
|
locality. | ||
|
||
Covered Type: A type which appears as a parameter to another type. For example, | ||
`T` is uncovered, but `Vec<T>` is covered. This is only relevant for type |
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 this say that "T
is covered in Vec<T>
" rather than "Vec<T>
is covered"?
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'd like to see this paragraph updated per @durka's notes.
Have we given up on specialization? If the new blanket impl is a default impl it can always be added backwards compatibly, right? |
In languages without coherence, the compiler has to have some way to choose | ||
which implementation to use when multiple implementations could apply. Scala | ||
does this by having complex scope resolution rules for "implicit" parameters. | ||
Haskell does this by picking one at random. |
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.
To be fair, it seems that Haskell solves the problem by erroring if you import modules with conflicting impls in them, unless you use a flag which is generally considered a terrible idea, in which case it can choose "arbitrarily".
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. The phrasing is not reflective of actual Haskell development. {-# LANGUAGE IncoherentInstances #-}
are pretty much a "never do this" and -fno-warn-orphans
is also considered bad practice.
cc @ekmett who gave the "Type Classes vs. the World" talk and @glaebhoerl.
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.
So for fairness towards the Haskell community and for accuracy, please note somehow that Haskell does have the coherence property. If you also want to elaborate on Scala, that's also fine; perhaps expand on this in a section on "Prior art" (which is missing btw...)
In pub trait Input {
fn process(&mut self, input: &[u8]);
} And ideally I would like to write blank impl for impl<T: Input> io::Write for T {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { .. }
} But obviously it can not be done under current coherence rules, thus I have to implement // `!io::Write` bound means that other crates can't implement
// both `Input` and `io::Write` on the same type
pub trait Input: !io::Write {
fn process(&mut self, input: &[u8]);
}
// But locally we can define blank impl of `io::Write`, because compiler
// guarantees that no one else can implement it for `T: Input`
impl<T: Input> io::Write for T {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { .. }
} |
@newpavlov Why can't you have something like this: pub struct InputWriter<T>(T);
impl<T: Input> Write for InputWriter<T> {} |
@abonander |
Is |
@tspiteri |
- `Target` is not an uncovered type parameter (is not any of `P1...Pn`) | ||
- At least one of the types `T1...Tn` must be a local type. Let `Ti` be the | ||
first such type. | ||
- No type parameters `P1...Pn` may appear *anywhere* in `T1...Tn` before `Ti`. |
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 "anywhere" is a bit ambiguous, as it could mean "as any of the type arguments" or "within any of the type arguments". (Though from the emphasis placed, and because the term "uncovered" is not used, I assume you mean the latter)
I might forego the emphasis and write
anywhere (even covered)
|
||
Rust's solution is to enforce that there is only one impl to choose from at all. | ||
While the rules required to enforce this are quite complex, the result is easy | ||
to reason about, and is generally considered to be quite important for Rust. |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
@sgrif Are you sure? Tuples are fundamental, iirc |
@leodasvacas Tuples are definitely not fundamental |
Sorry I've been slow to leave comments here. I am basically 👍 on this RFC. I had a few nitpicks on the phrasing, though I think others have mostly left similar comments; I'll have to go over my notes later. I did also have one particular question: I feel like the rules as stated in the RFC are unnecessarily narrow. The RFC states:
It surprised me to see the impl ForeignTrait1<LocalType> for ForeignType<T> but it forbids impl ForeignTrait2<ForeignType<T>, LocalType> for u32 Granted, the number of traits that include enough type parameters to make this relevant are few. Nonetheless, I think I would prefer to see the final rule without calling out
The major change from today being that we do not forbid type parameters "deeply" from Is there a reason we can't do this? It seems clear that this creates no more conflict with parent crates than existed before: for them to overlap with us, they must use an uncovered type parameter at position In terms of sibling or cousin crates, I think the argument is similar. Assume there are two cousin crates (neither is a (transitive) dependency of the other) with impls of the same trait Trait:
Is there a flaw in this reasoning? I guess the question is why we used the "no type parameters at all" rule in the first place. I think this has to do with historical context. It's worth remembering that these rules arose before RFC 1023 ("rebalancing coherence"), and in that setting we didn't have a notion of fundamental types. I remember that we were concerned about the symmetry of More generally, @sunjay and I have been working over in Chalk land on modeling the coherence rules and I think we're starting to make some nice progress. This work dovetails pretty nicely with this RFC. To keep this comment from getting too crazy long I'll hold off on commenting too much about it here. Maybe they can write more, or maybe I'll drop some tidbits later. The two main things we've been focused on are separating out the "axioms" (i.e., the things we define by fiat) -- from the "theorems" (i.e., the things we believe follow from that), as well as trying to state the rules in terms of chalk predicates. For example, there is a predicate |
@nikomatsakis: Um, did you mean to finish this sentence with something other than "and"? Your comment was really interesting to read otherwise...
|
@ErichDonGubler heh I suppose I did :) |
Changed to:
|
An impl of impl<T> ForeignTrait<LocalType> for ForeignType<T> Requires that cousin crates are not able to write the following impl impl<T> ForeignTrait<T> for ForeignType<CousinLocalType> If If |
So, taking fundamental types into account, the "new" rules could be: The predicate we want to define is "trait-ref R is orphan-owned by crate C". We split types into 3 categories:
The rules are:
ConnectionsAn impl Coherence can perform negative reasoning on a trait-ref R if either:
This is exactly the future-compatibility rules - adding an impl to a trait is future-compatible if that trait is non- Unbroken rulesThe "unbroken sequence of impl<T> RemoteTrait for FundamentalPair<LocalType<T>, Vec<LocalType<T>>> However, negative reasoning depends on the orphan rules, so any change in the orphan rules is a breaking change. Soundness proofTo show that this is sound, suppose that there is a trait-ref Suppose However, these types can't be local in |
Just so folks know, I'm on vacation but will reply in a few days |
@nikomatsakis you have one outstanding concern, "clarify-semver". Has this been addressed? |
@Centril that summary of Haskell's flags sounds like what I remember. I don't think we need to include that quantity of detail in the RFC, however. =) UPDATE Perhaps including a link to the excellent comment by @goldfirere would be nice though (comment) |
@rfcbot resolve clarify-semver Good enough I suppose. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
The final comment period, with a disposition to merge, as per the review above, is now complete. |
Huzzah! This RFC has been merged! Tracking issue: rust-lang/rust#55437 |
In cases like this, should the RFC that this supersedes get a prominent link to the RFC it is superseded by? |
*generally* we don't mess with older RFCs, but sometimes we do. it's
certainly not required.
…On Thu, Nov 1, 2018 at 5:16 AM Ralf Jung ***@***.***> wrote:
In cases like this, should the RFC that this supersedes get a prominent
link to the RFC it is superseded by?
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#2451 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AABsik4qo-PmBom9dntvrE2wbwtRpX8Qks5uqrwCgaJpZM4UT83X>
.
|
RFC #1023 introduced rules that exclude impls which should clearly be
valid. It also used some ambiguous language around what is a breaking
change, that ended up being completely ignored and contradicted by #1105.
This RFC seeks to clarify what is or isn't a breaking change when it
comes to implementing an existing trait, and conservatively expands the
orphan rules to allow impls which do not violate coherence, and fit
within the original goals of #1023.
Rendered
Tracking issue