-
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
Mutually exclusive traits #1148
Conversation
fn nourish(&self); | ||
} | ||
|
||
pub trait Poisonous: !Edible { |
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.
Do we require that both traits declare themselves to be negatively bounded by the other? It certainly improves readability, especially in larger source files. If this is the case, the compiler should generate a warning if Edible : !Poisonous
, but Poisonous
is not bounded by !Edible
.
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.
Edible: !Poisonous
implies Poisonous: !Edible
, so there wouldn't be a coherence violation if one denotation was left off. I agree that it is better readability for both to be denoted, and a lint would be good.
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.
Yes, a lint would certainly be the best way to do this.
Following @steveklabnik 's comment to my recent RFC submission, you should linebreak your Markdown. 👍 for the proposal |
| | impl ?Trait for T | | | | ||
| Default impl | by default | impl Trait for .. | impl !Trait for .. | | ||
| Bounds | by default | where T: Trait | where T: !Trait | | ||
| | where T: ?Sized | by default for Sized | | |
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 use code formatting for actual code and add more headers to clarify, like:
?Trait | Trait | !Trait | |
---|---|---|---|
Specific impl | by default | impl Trait for T |
impl !Trait for T |
Explicit impl | impl ?Trait for T |
impl Trait for T |
impl !Trait for T |
Default impl | by default | impl Trait for .. |
impl !Trait for .. |
Bounds | by default | where T: Trait |
where T: !Trait |
Explicit bound | where T: ?Sized |
by default for Sized |
I'm unsure about the "Explicit bound" header, though. Wouldn't that contradict your assertion that being bound by ?Trait makes no sense?
Changed the title to avoid confusion with mutexes |
This seems like something that may have some prior art in a language like Haskell (or some variant), so it'd be good to have a look around to see if there's been research/experiments with this somewhere already. I'm just a little nervous that this may have unexpected interactions so it'd be good to get a broader view. |
|
||
It may be difficult to grok the difference between `!Trait` and `?Trait`. The reason for this difference only becomes clear with an understanding of all the factors at play in the coherence system. Inferred `!Trait` impls and the rarity of `?Trait` impls should make this an unlikely corner of the trait system for a new user to accidentally happen upon, however. | ||
|
||
The `impl !Trait for T` syntax overlaps with the syntax of existing negative impls for types with default impls, and has slightly greater semantic content under this RFC tahn before. For each existing negative impl, it will need to be determined whether that type should impl `!Trait` or `?Trait` (that is, whether or not the non-implementation is a guarantee). That said, this change is not backwards incompatible and will not cause any regressions, and existing negative impls are an unstable feature outside of std. |
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.
There's a spelling error here: "under this RFC tahn before" should be "under this RFC than before"
This is very exciting and I hope it goes through. I would argue strongly in favor of the alternative proposal. If anyone is to use I feel I can best illustrate my point with a concrete example. Say that you want to create a linear algebra library as generically as possible, and you want to use trait Vector {}
struct Vec2<N> { x: N, y: N }
impl<N> Vector for Vec2<N> {}
// pairwise multiplication between vectors
impl<N: Mul<M>, M> Mul<Vec2<M>> for Vec2<N> {
type Output = Vec2<<N as Mul<M>>::Output>;
...
}
// scalar multiplication
impl<N: Mul<T>, T: !Vector> Mul<T> for Vec2<N> {
type Output = Vec2<<N as Mul<T>>::Output>;
...
} Under the alternative proposal, this is all that is needed and anyone can use your vectors with any type they wish. Under the main proposal, one would need to The workaround to this would be to include a second, essentially identical definition for scalar multiplication: impl<N: Mul<T>, T: ?Vector> Mul<T> for Vec2<N> {
type Output = Vec2<<N as Mul<T>>::Output>;
...
} This is clearly problematic. In fact, we could even forgo the first definition and only use the second, treating If creating trait |
@paholg Good point.
The RFC (line 126) proposes that |
@llogiq I missed that. I would argue that it is just more confusing, though, and would cause unexpected bugs if a crate is ever split into two. Within a crate, it's trivial to specify whether a type implements |
@paholg This rule actually reflects the current coherence rules as far as I understand. So if it really is confusing, we should look into how it is documented and what errors may ensue and make sure that the documentation and error messages clear up any confusion. However, I see why you'd like to have the default |
Note that the alternative is accessible a la carte by declaring
|
This part of the RFC is a clarification of the current rule, which was arrived at by evolution. Originally, types were |
In @paholg's example it'd also be possible to declare a mutually exclusive trait |
I looked around online & couldn't find anything like this in Haskell, so I asked in #haskell and it hasn't been considered. The only concern anyone in the channel raised had to do with orphan impls, which Haskell allows but Rust does not. I agree that this seems like something that could have surprising implications. I've tried hard to think of what they could be though and come up empty. |
Re: Local impls and warbling (using It actually is not necessary that local types and traits have the implicit relation EDIT: Actually, if you think of this 'warble' as just another kind of inference (if both type and trait are local, and two impls would overlap otherwise, infer that the type does not impl the trait), declaring the type fully |
Great, thanks for looking into it.
Yeah, I haven't thought of anything either, and I'm sure you've spent more time on it than me! |
I like this RFC, however, I have to admit that I'm afraid this could get too complicated (at least redundant).
Then you would have to declare non-meltability to all three types, but in real code this could become messy soon. (Imagine saying that Water is not Meltable, not Burnable, not Flameable and co, instead of saying what it does) I agree that both types should explicitly say their relationship and I also do not know a solution for this. |
@Anachron as long as |
@llogiq wouldn't that contradict the rule mentioned in #1148 (comment), that a missing declaration should at least trigger an info? |
Consolidated orphan rules stuff and implicit !Trait into a single section on inference
That would be a lint, not a hard-and-fast rule. Lints can be deactivated using Btw. that comment is not yet part of the RFC if I read it correctly. @withoutboats Shall I send you a PR to that effect? |
Your interpretation is correct. Similarly, there's no reason to support explicit impls of |
So, I'm long overdue in adding some detailed feedback here, and I apologize for that! Let me start by saying that I really like this approach at a high level. I find the motivation compelling. In particular, I think there is a need to finish the story that RFC 1023 began. Currently, negative reasoning is permitted within a crate, but not outside. This was a pragmatic rule at the time but it doesn't seem like the best rule going forward. For one thing, it's not clear that crate authors are aware of the negative reasoning they are using: it's easy to make accidental assumptions. For another, there are plenty of times when you'd be willing to promise never to implement a trait: for example, That said, I have some concerns. I'm going to start with a comment that kind of resummarizes the RFC and points out some quibbles with the text itself. I've got another in the works with some deeper concerns around negative reasoning, specialization, etc (not all of which are specific to this RFC). Part of my purpose in resummarizing the RFC is to ensure I am understanding the intention, so please correct me if you think I have something wrong! Marker traits vs new kinds of predicatesI find some parts of the RFC a bit confusing. Sometimes it seems to use
but, under this RFC, we would have two kinds of predicates:
(For now, I've excluded Furthermore, for non-structural (that is, non-OIBIT) traits, we now have one kind of impl:
but we would now have two, a positive and a negative impl:
The idea would be that we can prove the predicate Structural traitsNow we come to structural traits. Here we currently have three sorts of impls:
With structural traits today, things are somewhat inverted. To prove that
In other words, to determine whether Anyway, this RFC changes the story in two ways. First, it makes a subtle change to the semantics of
This The second change is that this RFC adds a negative
My interpretation of the language from this section is that, whereas I described the positive sense as generating a defualt positive impl, this would generate a default negative impl:
In other words, to determine whether I don't really see the purpose for this form as presented. What I have seen in the thread doesn't seem correct. For example, @withoutboats presents
where Relationship to fundamentalI think what the As I wrote, some of the comments seem to suggest that this is what Anyway, it is great that this RFC provides a somewhat more structured way to think about fundamental. We should maybe consider whether want to devise another syntax for it. (Or maybe just continue with the current unstable attribute.) When considering a possible alternative syntax, also note that we support fundamental types today. A fundamental type On the topic of
|
Thanks! It's been a few months since I thought about this RFC, this is fun. Any lack of clarity is certainly my fault as author and not the fault of any reader. Especially anything to do with I think what this RFC, structural traits (thank you for this term which is much better than OIBITs!) and specialization are all circling around is this: the relationship between traits and types is well-described in terms of set theory. Traits and type parameters both refer to sets of types. We currently have
The goal of this RFC is essentially to introduce disjunction in a manner that is consistent with these constraints (mainly the first one). I'm going to digest your comments more, especially on |
Another way to look at this is in terms of 'intuitionistic' vs classical logic. Basically, it is not the case that either
If the concern is primarily about the syntax, I certainly agree. The attribute was chosen because it the a simple and low-cost way to introduce the notion we needed. But nobody loves it. That said, there are a lot of places (e.g., Drop, negative impls, perhaps |
I wanted to follow up on this discussion about "intuitionistic" vs "classical" logic. The fact that there exist types where neither To see what I mean, imagine that I have two impls like this: impl<T:!Clone> SomeTrait for T { .. }
impl<T:Clone> SomeTrait for T { .. } In principle, this seems equivalent to these two impls (which rely on specialization): impl<T> SomeTrait for T { .. }
impl<T:Clone> SomeTrait for T { .. } But in fact this is not the case. The reason is that, in the specialization case, we know that
then I can't say whether What this means is that if I wanted to use methods from fn foo<T>(t: &T) { MyTrait::some_method(t); } I would have to write one of the following: fn foo<T:MyTrait>(t: &T) { MyTrait::some_method(t); }
fn foo<T:Clone>(t: &T) { MyTrait::some_method(t); }
fn foo<T:!Clone>(t: &T) { MyTrait::some_method(t); } In any case, I know this RFC explicitly says that specialization is not an alternative, but the wording there didn't explicitly draw out this point about the excluded middle, and I thought it was non-obvious. Also, as an aside, I don't fully agree with this characterization:
In particular, while this is true of the existing specialization RFC, there exist specialization variants where the subset relationship is not known to hold (and I personally think those variants could be very important for us -- but that's a comment for another RFC). |
So I wrote something: A modest proposal around negative reasoning and auto traits. I've been sitting on this write-up for a long time, hoping to edit it some more, but I've decided just to post it. As the title says, this is a "modest proposal" around negativity. I am not promoting this as an RFC yet -- for one thing, the material on auto traits is not quite right yet, though the differences are kind of in the weeds -- but it outlines my thinking about negativity quite well. Essentially, if we were to ever adopt a Frankly, I'm feeling pretty good about that proposal overall. But I know that others on @rust-lang/lang have some reservations. Let me try to summarize what I've heard:
|
It also lets you combine multiple blanket impls such as |
An example was posted in the subreddit today for which mutual exclusion would be well-suited, though it would also require another extension to the trait system surrounding trait Foo { }
impl<T> Foo for T where T: Copy { }
impl Foo for String { } The compiler disallows this (outside of the standard library) because it won't assume that no future version of The trait However there is a wrinkle in that
Both present a minor backward compatibility hazard in that removing all fields with a destructor is a breaking change. The first allows you to rely on that assumption in more cases. |
We discussed this RFC at the @rust-lang/lang meeting last week. We decided it makes sense to postpone this RFC for the time being, under issue #1053. Essentially, there isn't a lot of active conversation, and it makes sense to wait until the specialization implementation has progressed a bit farther before we make any decision here. As I wrote in my previous comment, though, I (personally) do think that there will be a role for negative reasoning, and that the broad outlines of this RFC are correct. |
Now that specialization is stable and released, what is the status on this RFC? |
Specialization is not stable. |
@oberien The ideas of this RFC are definitely something we're keeping in mind, but there are a lot of ways we might try to loosen the rules around coherence, so we're taking things a bit slowly. We're hoping to get specialization officially stabilized in the next couple of months; it's blocked on some pretty deep implementation work. After that, we can start looking more seriously at various extensions. |
@pitdicker just hit an issue needing negative type bounds or specialization (see here). |
@dhardy If a type cannot simultaneously implement trait BlockRng {
type Element;
type Results: AsRef<[Self::Element]> + Default;
}
impl<R> Rng for BlockRngWrapper<R>
where
R: BlockRng<Element=u32>,
R::Results: AsRef<[u32]> // <-- FIXME why is this line needed?
{ ... }
impl<R> Rng for BlockRngWrapper<R>
where
R: BlockRng<Element=u64>,
R::Results: AsRef<[u64]>
{ ... } Yes this still emits E0119 "conflicting implementations of trait", but at least we can guarantee that these two impls will never overlap using the more conservative #1672 instead of this RFC. (I guess all of these needs to wait for the |
Another example of the need for mutually exclusive trait bound, or negative trait bound, or specialization with lattice rules, or something else in the line. In it I try to write support code which will allow us to use let result = for value in values {
// body
}; To: let generator = IntoGenerator::into_generator(values);
let result = loop {
match generator.resume() {
GeneratorState::Yielded(value) => {
// body
},
GeneratorState::Complete(result) => break result,
}
}; Quite unfortunately it seems this approach is currently impossible... |
Mutually exclusive traits. Discussed previously on internals.rust-lang.org, but the RFC has been updated in material ways.
Rendered