-
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
Negative bounds #586
Negative bounds #586
Conversation
Hi @kennytm, thanks for the RFC. This is definitely something I'm interested in working through. I'm going to give this a detailed read as soon as I can and give you some detailed feedback. |
Very well done and comprehensive RFC 👏 |
Syntax bikeshed: Use |
@P1start: Thanks, I've added |
This seems to mean that Furthermore I cannot really think of any time it would ever be necessary or even useful to have negative lifetime bounds. Could you give an example? |
No. It guarantees I included negatived lifetime bounds just for completion. I think the only useful example is to differentiate between impl Trait for &'static str { ... }
impl<'a: !'static> Trait for &'a str { ... } |
For negative projection bounds, I see why negation can't happen on the outside position, but is there a reason this shorthand couldn't work? where T: Iterator<Item != u8> It would desugar into |
|
||
### Inequality bounds | ||
|
||
Instead of `where T != u8`, we may write `where T: !u8` for an inequality bound. The advantage is we could add inequality to multiple types much more concisely: |
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 wouldn’t really work because trait objects are types, so T: !Reader
would be ambiguous between specifying that T
does not implement Reader
and that T
is not an unsized Reader
trait object 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.
Ah, thanks. Looks like I could remove this section then.
…trait. Added shorthand `T: Trait<Item != E>`.
@tomjakubowski : Yes it could work. Added it to the text too. |
What happens if I do this? trait A {}
trait B {}
impl A for .. {}
impl<T> !A for T where T: B {}
impl B for .. {}
impl<T> !B for T where T: !A {}
struct Foo; Does |
|
||
In Rust, all traits are open for extension. Any traits with no superbounds are able to be implemented by the same type, even if the original developer may not think they should be used together. Therefore, all traits with no superbounds should be considered overlapping. | ||
|
||
The only way to create disjoint collection of trait bounds is by negative bounds. It is guaranteed that `!B` and `B` share no common types. In a collection `T: A + B + C + …`, as long as two of them are disjoint, the whole collection is also disjoint. |
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 understand what you mean in the second sentence here, but the language could be clarified.
@theemathas: Interesting. Ideally the compiler should be able to recognize the contradiction and reject the program. However, this is more about the problem of negative impls: // One trait:
trait X {}
impl X for .. {}
impl<T> !X for T where T: X {}
// Three traits:
trait A {}
trait B {}
trait C {}
impl A for .. {}
impl B for .. {}
impl C for .. {}
impl<T> !A for T where T: B {}
impl<T> !B for T where T: C {}
impl<T> !C for T where T: A {} Your example should behave the same as these two, be it |
Is it proposed to write specialization like this? impl<T: !bool> MyVec<T> {}
impl<T: bool> MyVec<T> {} // different impl only for MyVec<bool> That is, specify the type name instead of a trait name in the bound. |
@m13253 You use (in)equality bounds for specific types: impl<T> MyVec<T> where T != bool {}
impl MyVec<bool> {} |
Gah. I haven't had time to really dig into this, but I have to and want to, because the inability to implement However, one thing I did want to weigh in on: I am not in favor of adding any kind of negative region bounds at the moment. Region inference is complicated enough without trying to consider bounds of this kind. Now, granted, we could probably enforce these negative bounds as a kind of "after-effect", where we search over the results of inference (which will naturally try to infer the smallest region they can) and just check whether or not the negative bounds hold. But I'd still rather hold off absent a clear use-case, on the grounds of keeping future options open and limiting complexity. |
Also, we haven't implemented support for equality bounds yet, and for good reasons, so I wouldn't want to officially support inequality bounds. They also have interactions with inference (which is currently driven by unification). (That said, just like region bounds, I suspect we could eventually support these as a kind of "after-the-fact" check, rather than having them inform inference in any way.) |
@nikomatsakis Can you elaborate on this a little? Just wondering what the blockers are for equality bounds. I've poked around in the code a bit and briefly considered trying to implement some of the missing stuff for Also, has any consideration been given to switching away from traditional unification toward a hopefully simpler and more flexible bidirectional algorithm? I'm thinking of something like what is described in this paper. Their particular approach is predicative although they discuss some options for impredicativity. |
So I spent some time reading the RFC yesterday and thinking about it. What is written makes a lot of sense, but there are a lot of issues that I think arise that must be thought through. Also, I had some interest in pursuing negative bounds as a solution to rust-lang/rust#18835, but I am now not sure that they will help there (see below). All in all I feel like this is a fairly non-trivial extension to the trait system that I do not want to rush into. (I've thought otherwise in the past. After all, there are parts of the trait system -- in particular some aspects of coherence -- that rely on the ability to decide that a trait is definitively not implemented, so in some sense we have negative bounds already, but in thinking about it more I've decided that adding negative bounds as a first-class thing opens a lot of new questions.) Here are some (preliminary) thoughts: Negative bounds as the way to do specializationIt's clear that negative bounds potentially enable a certain amount of specialization. However, I don't consider them a full solution, because they leave a number of important use cases unaddressed. The RFC mentions some of these concerns. The biggest concern is cross-crate specialization: I'd like to be able to define blankets in one crate and specialize them in others. There are also ergonomic concerns. Finally, specialization might allow us to enforce additional interesting constraints, like saying that all specializations of a "base impl" have consistent values for associated types (this relates to the next section). Interactions with the current implementationCurrently when resolving a trait obligation in the type checker, we always ensure that we can pick a specific impl. This makes sense because the impl defines the output type parameter definitions. It also makes sense because, not infrequently, some of the input type parameters are also being inferred, so narrowing down to a particular impl lets us know the values of those type parameters as well (we infer this based on the set of impls that are in scope). After type-checking is done, the precise set of impl type parameters are known. Thanks to coherence, this is enough to guarantee that later, when generating code, we can replay that search and always end up with the same impl (even when inlined into downstream crates). Using negative bounds will not necessarily interact smoothly with this kind of specialization. To adapt an example from the RFC, imagine that we have: trait Foo { type Output; fn foo(&self) -> Self::Output; }
impl<T:Int+!Float> Foo for T { type Output = i32; ... }
impl<T:Float> Foo for T { type Output = f32; ... } Now imagine I have a function: fn call_foo<T:Int>(t: T) { t.foo() } If we adapt the code in a kind of straight-forward way, the function fn call_foo<T:Int+Foo>(t: T) { t.foo() } At least as currently implemented, this would cause Another option would be to specify enough bounds to narrow down to one impl: fn call_foo<T:Int+!Float>(t: T) { t.foo() } Impact on
|
@darinmorrison sorry, tuckered myself out with that last comment ;) I'll try to respond later. As for the suggested alternative inference scheme, I'm not familiar with that work, but I just printed it out. |
@nikomatsakis Thanks for the comment 😄 About the
|
One thing I forgot in my big comment: in addition to dealing with associated types, another reason that the type checker wants to identify a particular impl is that it has to check that all the where-clauses on that impl hold. If it were to somehow avoid deciding between the |
@kennytm yes, a very good point about Ignoring the problem with @aturon had an idea that could provide a solution to the object-safety/Sized question. The basic idea was to say that methods which have a where-clause requiring that Regarding your proposed Option E ( Whether or not the fn requires a where clause is a bit unclear. Probably it does if we interpret the current rules strictly, because it's needed to validate that the |
I guess besides ergonomics, the shortcoming of "inheritance" for |
My feeling is that we should have both inheritance and forwarding (A more out-there idea is that if we had the ability to parameterize over the capabilities of references - shared,
Also a gut feeling, but it feels like this should fall afoul of the orphan rules - it's neither their type nor their trait, after all. But I don't have the whole coherence/orphans debate inside my head with respect to what desirable patterns would be collateral damage. |
Or maybe I'm not getting the point? |
@m13253 Compiler shoud decide which impl to use before a generic function is used because when an error is reported for the using, that should be only because the type parameters don't match the limitation of bounds. Otherwise there should already been an error for the impl. |
Isn't it a big backwards compatibility hazard in general? The reason is that with negative trait bounds, you can “fence in” the type space completely, so a particular trait can be implemented (in various ways) for every type that exists. That means that if a published type anywhere makes a “transition” from not implementing a trait to implementing it, it may directly break some code. |
@bluss: I don't quite understand. I believe a similar issue already happens with default impl + negative impl? |
The following is an example program of what user code will look like. It demonstrates that types that are part of a public API cannot add new trait impls without that being a breaking change. I believe that today // use std::ops::Range;
// use std::slice::Chunks;
trait IsShow { fn number(&self) -> i32 }
impl<T: !Debug> IsShow for T {
fn number(&self) -> i32 { -1 }
}
impl<T: Debug> IsShow for T {
fn number(&self) -> i32 { 1 }
}
fn main() {
let total = (0..1).number() + [1,2,3].chunks(2).number();
if total == 0 {
println!("It's a balance");
} else {
println!("Something has changed");
}
} That was breaking behaviour, but breaking compilation is easier without “fencing in” the type space. This example will stop compiling if you add new trait impls to libstd: trait IsShow { fn number(&self) -> i32 }
impl<T: !Debug> IsShow for T {
fn number(&self) -> i32 { -1 }
}
fn main() {
let total = [1,2,3].chunks(2).number();
} |
@bluss I don't find this convincing, as the example is using Also I think "runtime" detection of whether a trait is implemented can be a valid behavior, e.g. fn show_ptr<T>(ptr: &T) -> String {
if is_fmt_debug::<T>() {
format!("{}", *ptr.as_fmt_debug())
} else {
format!("{:p}", ptr)
}
} |
What the code does is just an example. The point is that we introduce a major backwards compatibility hazard and need to discuss whether that's something we want in Rust. We can't remove trait impls backwards compatibly today, with negative trait bounds, we can't add trait impls backwards compatibly. |
@bluss indeed, it looks like you and I were exploring similar thoughts in parallel. |
It's been a while since this RFC was opened and I wanted to note a few related developments that have occurred in the meantime:
|
I think we are not ready to move forward on this just now. There is too much in flight. Therefore, I'm going to close this RFC as postponed and file it under the existing issues #442 and #290. Thanks @kennytm for the RFC and others for the good points raised here, I feel confident we will come back to this point. |
Can this be reopened? |
afaik this is now subsumed by #1053, and the status is still that we're waiting for specialization and other things to stabilize anyway |
51: Fix #49 r=cuviper a=maxbla fixed issue #49 added a test case for it Side note: `cmp` is a bit scary. Do we know what it's amortized runtime is? It seems to me that the worst case (having to compare the reciprocals many times) could be pretty bad. Is there any way to have a separate `impl` for `T: Clone + Integer + CheckedMul`? As the comments note, CheckedMul would make the implementation faster. I messed around, but I can't find a way to have two implementations -- one for checked and one for no checked. I found an [this](rust-lang/rfcs#586) RFC for negative trait bounds though (spoiler: negative trait bounds are not happening soon). Co-authored-by: Max Blachman <blachmanmax@gmail.com>
I think this rule could help with backwards compatibility changes of negative trait bounds. In summary it would forbid negative trait implementations without their complementary counterpart. So you would have code like this: impl<T: A + !B> Foo for T {
// ...
}
// required by the previous impl block
impl<T: A + B> Foo for T {
// ...
} |
Executive summary:
Rendered
cc #442, #290, rust-lang/rust#19032.