-
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
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,174 @@ | ||||||||||||||||||||||||||
- Feature Name: mutex_Traits | ||||||||||||||||||||||||||
- Start Date: 2015-06-02 | ||||||||||||||||||||||||||
- RFC PR: (leave this empty) | ||||||||||||||||||||||||||
- Rust Issue: (leave this empty) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
# Summary | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Introduce a distinction between `!Trait` and `?Trait` to enable mutually exclusive traits within the constraint that implementing a trait for a type should be a backwards compatible change. With these two type - trait relations distinguished from one another, allow negative impls of all traits as well as negative bounds, introducing mutual exclusion to the trait system. This enables users to encode additional logic in types & to provide multiple coherent blanket impls for parameterized types with mutually exclusive trait bounds, avoiding the backward compatibility problems that have hampered similar proposals in the past. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
# Motivation | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Trait coherence rules ensure that the compiler will predictably associate a particular impl with any invocation of a method or item associated with that trait. Without coherence rules, the behavior of a Rust program could easily become disastrously non-deterministic. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
However, the current coherence rules are very conservative in what they allow to be implemented. Other RFCs may introduce allowances for multiple overlapping implementations using an order of precendence for specialized implementations. This RFC addresses the limitation from another angle. By introducing mutually exclusive traits, it becomes possible to declare that two traits must not both be implemented by a single type, causing parameters bound by each of those traits to be non-overlapping. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Take this system, which defines `Consumable`, `Edible` and `Poisonous` traits. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
```rust | ||||||||||||||||||||||||||
pub trait Consumable { | ||||||||||||||||||||||||||
fn consume(&self); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
pub trait Edible: !Poisonous { | ||||||||||||||||||||||||||
fn nourish(&self); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
pub trait Poisonous: !Edible { | ||||||||||||||||||||||||||
fn sicken(&self); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
impl<T> Consumable for T where T: Edible { | ||||||||||||||||||||||||||
fn consume(&self) { self.nourish() } | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
impl<T> Consumable for T where T: Poisonous { | ||||||||||||||||||||||||||
fn consume(&self) { self.sicken() } | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Though logic of this sort can be implemented with ADTs, they can not be implemented using traits, which are importantly extensible in a way that ADTs aren't. Rust gains a great deal of expressiveness, composability, and power from the ability of two unrelated crates to extend the capabilities of types from a third crate they both depend on. For example, two crates which depend on std can both extend `Vec<T>`, and a client of both those crates can compose the behaviors they implement. However, this kind of 'pivot' cannot be performed on traits by, for example, crate A implementing a trait from std for its new type, and crate B implementing a new trait for all types which implement that same trait. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
In addition to the coherence benefits of this change, disallowing a single type to implement two traits has benefits in itself. By encoding mutual exclusivity in the type system, programmers can implement a greater portion of the program logic in a statically analyzable manner. For example, the `num` crate currently defines two traits `Signed` and `Unsigned`. `Unsigned` is a marker trait which exists only to indicate that a type is not `Signed`, but the type system will allow types to implement both `Signed` and `Unsigned` without objection. Clients of `num` cannot actually rely on an `Unsigned` bound to guarantee that a type is not `Signed`, even though that is the trait's only purpose. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
# Detailed design | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
## Trait, !Trait, and ?Trait | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
An earlier RFC attempted to codify negative reasoning ran aground on the problem of backward compatibility. If it is possible to bound a type parameter or trait by the non-implementation of another trait, then that non-implementation - the _absence_ of code - becomes a semantic expression. As an example, consider this system, with traits `Consuamble`, `Edible`, and type `Dirt`. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
```rust | ||||||||||||||||||||||||||
pub trait Consumable { | ||||||||||||||||||||||||||
fn consume(&self); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
pub trait Edible { | ||||||||||||||||||||||||||
fn nourish(&self); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
pub type Dirt; | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
impl<T> Consumable for T where T: Edible { | ||||||||||||||||||||||||||
fn consume(&self) { self.nourish() } | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
impl<T> Consumable for T where T: !Edible { | ||||||||||||||||||||||||||
fn consume(&self) { } | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Under the current definitions, consuming `Dirt` has no effect, but if we were to later to discover a way to implement `Edible` for `Dirt`, that would change, and the behavior of any client relying on `Dirt`'s implementation of `Consumable` wouild change. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
A solution to this problem which would enable mutual exclusivity is to hold that the relation between types and traits is in one of three states for every type and trait, rather than two: | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
* `T: Trait` - The type `T` impleemnts the trait `Trait`. | ||||||||||||||||||||||||||
* `T: !Trait` - The type `T` does not implement the trait `Trait`. | ||||||||||||||||||||||||||
* `T: ?Trait` - The type `T` may or may not implement the trait `Trait`. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Without a contrary expression, the relationship between any `T` and any `Trait` is `T: ?Trait`. This table documents the three relations and how they are described: | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| | ?Trait | Trait | !Trait | | ||||||||||||||||||||||||||
|---------------|-------------------|----------------------|--------------------| | ||||||||||||||||||||||||||
| Specific impl | by default | impl Trait for T | impl !Trait for T | | ||||||||||||||||||||||||||
| | impl ?Trait for T | | | | ||||||||||||||||||||||||||
| Default impl | by default | impl Trait for .. | impl !Trait for .. | | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note, that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am aware of the difference between default impls and unbound blanket impls, you'll note I specify how the rules for default impls relate to this proposal later in the RFC. The semantics of default impls do not change under this RFC and there is no need for alternative notation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, sorry, now I see:
This indeed corresponds to the current behaviour of default impls, but it is an undesirable property for a general purpose trait. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's correct & is a limitation on the usefulness of default impls (another is that they are currently only allowable for marker traits). It doesn't really relate to this RFC though? |
||||||||||||||||||||||||||
| 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 commentThe 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:
I'm unsure about the "Explicit bound" header, though. Wouldn't that contradict your assertion that being bound by ?Trait makes no sense? |
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
## Defining ?Trait and !Trait | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
`?Trait` and `!Trait` act as if they were marker traits. They define no methods and have no associated items. They are defined implicitly in the same scope as the definition of `Trait` and imported wherever `Trait` is imported. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
## Implementing ?Trait | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
`?Trait` can only be implemented for types for which a default impl has been explicitly defined (e.g. `Send` and `Sync`). Explicit default impls of `?Trait` are not allowed. `?Trait` is implemented by default anyway, and it would not make sense to implement it except in the cases where an explicit default impl exists. As a rule, the syntax `?Trait` will be very uncommon. | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit unclear to me. So There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A type which implements Bounding by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So impl |
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
`?Trait` follows the same orphan rules as `Trait`. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
## Implementing !Trait | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Implementing `!Trait` for `T` is a forward-compatible guarantee that `T` does not and will not implement `Trait`. This makes negative reasoning explicit and avoids backwards compatibility hazards. It goes without saying that it would be a coherence violation for a single type to implement both `Trait` and `!Trait`. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
`!Trait` follows the same orphan rules as `Trait`. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
## Bounding by !Trait | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Bounding by `!Trait` requires that types _explicitly implement_ `!Trait` in order to meet that bound. As mentioned prior, this avoids the hazard that implicit negative reasoning introduces. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
## Syntactic sugar: Implicit `!Trait` inference | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
If a type `T` implements a trait `Foo` which is bounded `!Bar`, an implementation of `!Bar` is inferred for `T` (unless `T` explicitly implements `!Bar`, of course). This avoids boilerplate negative impls which are inferrable from other impls for the type. | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a very cool feature, but looks a bit like spooky action from a distance. We should make sure that error messages (e.g. if someone tries to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with this. If someone attempts to implement as an example, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I dislike this feature, and I preferred it being left out in the original draft. With a positive trait bound on a trait I have to manually implement it, I can't see why that would be different with a negative trait bound. I would expect/prefer an error like:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thinking about it a bit more, @jnicklas proposal certainly meshes well with the "make code semantically complete locally" philosophy of Rust (which is also why we only have fn-local type inference). Someone reading Requiring that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'd definitely be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I chose to propose that sugar because it seemed like there wasn't enough in the pre-RFC for folks to disagree about. :-) In seriousness, I do think this is good sugar. It's not actually true that you have to manually implement all positive traits which are bounds, because some are implemented by default through default impls. Default impls currently can only be provided for marker traits, and of the four non-built-in traits in std::marker ( If a type implements a trait, it cannot implement a trait which is mutually exclusive with that trait. Implementing that trait is a semantic declaration that this type does not implement traits that are mutually exclusive with the traits it does implement. Mutual exclusion implies that these traits are fundamentally disparate (e.g. It seems much less of a leaping inference than the leap that Rust already makes in local contexts, which makes this code compile (and which cannot be removed). pub trait Quux { }
pub struct Foo;
pub trait Bar { }
impl Quux for Foo { }
impl<T> Quux for T where T: Bar { } There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you want to stand by it, at least extend the Alternatives section to cover @jnicklas' proposal. I'm currently leaning in favor of explicit stating the impl, because we can later remove the requirement in a backwards-compatible way. Introducing the requirement later would break code. |
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
## Clarification of default impl rules | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
If a default impl of `Trait` exists, these rules are used to determine the relation between `T` and `Trait`: | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
* If `Trait`, `?Trait` or `!Trait` is implemented for `T`, that impl defines the relation | ||||||||||||||||||||||||||
* If one of the members of `T` impls a trait which conflicts with the default impl, `T` is `?Trait` | ||||||||||||||||||||||||||
* Otherwise, `T` implements the default impl. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Note that this set of rules is sound if we suppose that every trait has an implicit default impl of `?Trait`. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
## Orphan rule warbles | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
The rules above all apply to a Rust system as a whole, composed of multiple crates associated as a directed acyclic graph. Within crates and modules, orphan rules allow silence to have a semantic expressions that is slightly different from these rules. Unfortunately, eliminating this warble would be backwards incompatible. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Specifically, when both a trait and a type are defined within a single crate, that type and trait have the relationship `T: !Trait` by default, rather than `?Trait`, only within that crate. This allows a certain degree of implicit negative reasoning which cannot be performed outside of that local context. It does not present a logical contradiction for this proposal. | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to have "unless T actually has an |
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
# Drawbacks | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
This adds rules to Rust's trait coherence system. Adding rules to the language makes it less accessible, and is always a drawback. There is a trade off here between easiness and expressiveness. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
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 commentThe 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" |
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
# Alternatives | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
## Sibling proposal: !Trait by default | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
There is an alternative scheme which has some advantages and disadvantages when compared to that proposed in the main RFC. I am mostly certain that the main proposal is the better one, but I have included this for a complete consideration. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Under this alternative, types would impl `!Trait` by default, and a default implementation of `?Trait` would be necessary to make that not the case. The table for such a proposal would look like this: | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| | ?Trait | Trait | !Trait | | ||||||||||||||||||||||||||
|---------------|--------------------|----------------------|-------------------| | ||||||||||||||||||||||||||
| Specific impl | impl ?Trait for T | impl Trait for T | by default | | ||||||||||||||||||||||||||
| | | | impl !Trait for T | | ||||||||||||||||||||||||||
| Default impl | impl ?Trait for .. | impl Trait for .. | by default | | ||||||||||||||||||||||||||
| Bounds | by default | where T: Trait | where T: !Trait | | ||||||||||||||||||||||||||
| | except: ?Sized | by default for Sized | | | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
The trade off at play here is between these two desirable and incompatible features: | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
* Adding new implementations should be backwards compatible. | ||||||||||||||||||||||||||
* Implementations for `T: Trait` should not overlap with implementations for types that don't | ||||||||||||||||||||||||||
implement Trait. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Under this alternative proposal, types would be implicitly non-overlapping with traits they do not implement, but it would also be backwards incompatible to implement new traits for types unless the trait's author has specified that it should be. Because the author is unlikely to know if anyone will want to add new implementations in a backwards compatible way, traits implementing `?Trait` by default is preferred. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
## Other alternatives | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Allowing negative bounds without distinguishing `!Trait` and `?Trait` remains an alternative, but it presents a backward compatibility hazard as discussed above. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Doing nothing is also an alternative; this would mean that traits cannot be declared to be mutually exclusive. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
## Not an alternative: specialization | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
As an aside, this RFC does not overlap with proposals for trait specialization. Mutual exclusion is useful for situations in which specialization would not be possible, and the same is true of the reverse. Put in terms of sets, traits declare sets of types; mutually exclusive traits are disjoint sets, and specialized implementations are subsets. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
Conceptually, they are connected in that they expand what is allowed by Rust's coherence system, but their use cases are separate and distinct. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
# Unresolved questions | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
This RFC does not attempt to address how mutual exclusion would be applied to the types and traits in std and other Rust-lang sponsored crates. This should be the subject of one or more separate 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.
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
, butPoisonous
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
impliesPoisonous: !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.