Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upRe-Rebalancing Coherence #2451
Conversation
Centril
added
the
T-lang
label
May 30, 2018
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
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). |
cramertj
reviewed
May 30, 2018
| 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.
cramertj
May 30, 2018
•
Member
Edit: disregard-- this is disallowed because of missing negative reasoning in the conflict checker, not because of coherence.
If i understand correctly, this formulation of the rules also allows things like this:
trait A<P> {}
trait B<P> {}
impl<P> B<P> for i32 {}
impl<P, T: A<P>> B<P> for T {}The first of these conditions (Trait is a local trait) is satisfied because B<P> is defined in the current crate, so the impl is allowed.
I originally brought up my desire to do this long ago in this thread, and @nikomatsakis pointed out that allowing this impl means that downstream traits have to check for conflicts created by a combination of local and non-local impls, which was undesirable. @nikomatsakis can you elaborate on the issues you thought this would cause?
This comment has been minimized.
This comment has been minimized.
sgrif
May 30, 2018
•
Author
Contributor
@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
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
rosekunkel
commented
May 31, 2018
Just to be clear, these are not meant to be the same |
This comment has been minimized.
This comment has been minimized.
warlord500
commented
May 31, 2018
|
awesome write up.
I think you should simplify it to
|
durka
reviewed
May 31, 2018
| 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 |
This comment has been minimized.
This comment has been minimized.
durka
May 31, 2018
Contributor
Should this say that "T is covered in Vec<T>" rather than "Vec<T> is covered"?
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
abonander
commented
May 31, 2018
Have we given up on specialization? If the new blanket impl is a default impl it can always be added backwards compatibly, right? |
durka
reviewed
May 31, 2018
| 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. |
This comment has been minimized.
This comment has been minimized.
durka
May 31, 2018
Contributor
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".
This comment has been minimized.
This comment has been minimized.
Centril
May 31, 2018
Contributor
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.
This comment has been minimized.
This comment has been minimized.
Centril
Sep 23, 2018
Contributor
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...)
This comment has been minimized.
This comment has been minimized.
newpavlov
commented
May 31, 2018
•
|
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> { .. }
} |
This comment has been minimized.
This comment has been minimized.
abonander
commented
May 31, 2018
|
@newpavlov Why can't you have something like this: pub struct InputWriter<T>(T);
impl<T: Input> Write for InputWriter<T> {} |
This comment has been minimized.
This comment has been minimized.
newpavlov
commented
May 31, 2018
|
@abonander |
This comment has been minimized.
This comment has been minimized.
tspiteri
commented
May 31, 2018
Is |
This comment has been minimized.
This comment has been minimized.
|
@tspiteri |
ExpHP
reviewed
May 31, 2018
| - `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`. |
This comment has been minimized.
This comment has been minimized.
ExpHP
May 31, 2018
•
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)
Centril
reviewed
May 31, 2018
|
|
||
| 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.
Centril
May 31, 2018
Contributor
Important yes; easy to reason about... not so much; or at least, I've never felt it was so easy to tell what impls will be accepted and which will be rejected.
This comment was marked as resolved.
This comment was marked as resolved.
Diggsey
May 31, 2018
Contributor
I think it means that it's easy to reason about the behaviour of the code (because there can only be one possible implementation) rather than the coherence rules themselves being easy to reason about.
This comment was marked as resolved.
This comment was marked as resolved.
dwijnand
May 31, 2018
Member
Yeah, the result is easy to reason about, but not necessarily the rules.
This comment was marked as resolved.
This comment was marked as resolved.
This comment has been minimized.
This comment has been minimized.
leodasvacas
commented
Jun 1, 2018
@sgrif Are you sure? Tuples are fundamental, iirc |
This comment has been minimized.
This comment has been minimized.
|
@leodasvacas Tuples are definitely not fundamental |
This comment has been minimized.
This comment has been minimized.
|
Sorry I've been slow to leave comments here. I am basically 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 u32Granted, 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 |
This comment has been minimized.
This comment has been minimized.
ErichDonGubler
commented
Jun 22, 2018
|
@nikomatsakis: Um, did you mean to finish this sentence with something other than "and"? Your comment was really interesting to read otherwise...
|
This comment has been minimized.
This comment has been minimized.
|
@ErichDonGubler heh I suppose I did :) |
This comment has been minimized.
This comment has been minimized.
|
Changed to:
|
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
|
Just so folks know, I'm on vacation but will reply in a few days |
scottmcm
assigned
nikomatsakis
Jul 5, 2018
This was referenced Jul 7, 2018
This comment has been minimized.
This comment has been minimized.
|
@sgrif I don't think that's true.. an impl is breaking if it conflicts with an impl a downstream crate is allowed to write. If a downstream crate couldn't write an impl before, but now it can, that makes more impls breaking. That is, I'm considering "breaking" de facto based on code being compileable against one version and not the next, not de jure based on what rules an RFC laid out as a breaking change. |
This comment has been minimized.
This comment has been minimized.
|
@withoutboats Fair point. Either way I agree that a window for the libs team is a good idea. |
This comment has been minimized.
This comment has been minimized.
|
@sgrif sounds great, wanted to make sure I wasn't misunderstanding something :) |
This comment has been minimized.
This comment has been minimized.
goldfirere
commented
Oct 10, 2018
|
Clarifying a bit about #2451 (comment) only. I'm not familiar enough with Rust to have any opinion at all about this proposal; please do not take these comments as such. I'm just trying to present the facts about GHC as I see them. In agreement with other comments, I'll address GHC, which is more relevant than what is stated in any Haskell Report.
I hope this is helpful! |
This comment has been minimized.
This comment has been minimized.
|
Fantastic RFC! It's a great insight that the current rules don't actually enable the parent crate changes you might hope for -- I wonder how this was missed the first time around. In any case, the reasoning looks solid to me, as does the formulation. I'm excited to see this land. @rfcbot reviewed |
This comment has been minimized.
This comment has been minimized.
|
@nikomatsakis you have one outstanding concern, "clarify-semver". Has this been addressed? |
This comment has been minimized.
This comment has been minimized.
|
@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) |
This comment has been minimized.
This comment has been minimized.
|
@rfcbot resolve clarify-semver Good enough I suppose. |
rfcbot
added
final-comment-period
and removed
proposed-final-comment-period
labels
Oct 17, 2018
This comment has been minimized.
This comment has been minimized.
rfcbot
commented
Oct 17, 2018
|
|
mitchmindtree
referenced this pull request
Oct 18, 2018
Closed
Allowing for nodes with "generic" inputs and outputs. #2
This comment has been minimized.
This comment has been minimized.
rfcbot
commented
Oct 27, 2018
|
The final comment period, with a disposition to merge, as per the review above, is now complete. |
rfcbot
added
finished-final-comment-period
and removed
final-comment-period
labels
Oct 27, 2018
Centril
referenced this pull request
Oct 28, 2018
Open
Tracking issue for RFC 2451, "Re-Rebalancing Coherence" #55437
Centril
approved these changes
Oct 28, 2018
Centril
merged commit 37b657e
into
rust-lang:master
Oct 28, 2018
This comment has been minimized.
This comment has been minimized.
|
Huzzah! This RFC has been merged! Tracking issue: rust-lang/rust#55437 |
This comment has been minimized.
This comment has been minimized.
|
In cases like this, should the RFC that this supersedes get a prominent link to the RFC it is superseded by? |
This comment has been minimized.
This comment has been minimized.
|
*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>
.
|
sgrif commentedMay 30, 2018
•
edited by Centril
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