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 upNegative bounds #586
Conversation
nikomatsakis
self-assigned this
Jan 15, 2015
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
Very well done and comprehensive RFC |
This comment has been minimized.
This comment has been minimized.
|
Syntax bikeshed: Use |
This comment has been minimized.
This comment has been minimized.
|
@P1start: Thanks, I've added |
This comment has been minimized.
This comment has been minimized.
scialex
commented
Jan 16, 2015
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? |
This comment has been minimized.
This comment has been minimized.
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 { ... } |
This comment has been minimized.
This comment has been minimized.
|
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 |
P1start
reviewed
Jan 17, 2015
|
|
||
| ### 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: |
This comment has been minimized.
This comment has been minimized.
P1start
Jan 17, 2015
Contributor
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.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
@tomjakubowski : Yes it could work. Added it to the text too. |
This comment has been minimized.
This comment has been minimized.
theemathas
commented
Jan 19, 2015
|
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 |
reem
reviewed
Jan 19, 2015
|
|
||
| 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. |
This comment has been minimized.
This comment has been minimized.
reem
Jan 19, 2015
I understand what you mean in the second sentence here, but the language could be clarified.
This comment has been minimized.
This comment has been minimized.
|
@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 |
P1start
referenced this pull request
Jan 29, 2015
Merged
(It turned out to be Un-)sound Generic Drop #769
lfairy
referenced this pull request
Jan 30, 2015
Closed
"".to_string() is slower than String::new() #18404
This comment has been minimized.
This comment has been minimized.
m13253
commented
Jan 30, 2015
|
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. |
This comment has been minimized.
This comment has been minimized.
|
@m13253 You use (in)equality bounds for specific types: impl<T> MyVec<T> where T != bool {}
impl MyVec<bool> {} |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
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.) |
This comment has been minimized.
This comment has been minimized.
@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. |
This comment has been minimized.
This comment has been minimized.
|
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
|
This comment has been minimized.
This comment has been minimized.
|
@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. |
This comment has been minimized.
This comment has been minimized.
|
@nikomatsakis Thanks for the comment
|
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
|
@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 |
This comment has been minimized.
This comment has been minimized.
|
I guess besides ergonomics, the shortcoming of "inheritance" for |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
m13253
commented
Feb 7, 2015
Or maybe I'm not getting the point? |
This comment has been minimized.
This comment has been minimized.
bombless
commented
Feb 7, 2015
|
@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. |
This comment has been minimized.
This comment has been minimized.
bluss
commented
Feb 10, 2015
|
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. |
This comment has been minimized.
This comment has been minimized.
|
@bluss: I don't quite understand. I believe a similar issue already happens with default impl + negative impl? |
This comment has been minimized.
This comment has been minimized.
bluss
commented
Feb 11, 2015
|
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();
} |
This comment has been minimized.
This comment has been minimized.
|
@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)
}
} |
This comment has been minimized.
This comment has been minimized.
bluss
commented
Feb 11, 2015
|
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. |
SSheldon
referenced this pull request
Feb 13, 2015
Closed
encode should return ? for types that don't implement Encode #11
This was referenced Mar 25, 2015
This comment has been minimized.
This comment has been minimized.
|
@bluss indeed, it looks like you and I were exploring similar thoughts in parallel. |
This comment has been minimized.
This comment has been minimized.
|
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:
|
This comment has been minimized.
This comment has been minimized.
|
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. |
nikomatsakis
closed this
Apr 10, 2015
nikomatsakis
referenced this pull request
Apr 10, 2015
Open
More flexible coherence rules that permit overlap #1053
withoutboats
referenced this pull request
Jun 27, 2016
Closed
Revisiting specialization: Complementary traits #1658
This comment has been minimized.
This comment has been minimized.
Laaas
commented
Aug 21, 2018
|
Can this be reopened? |
Centril
added
the
postponed
label
Aug 21, 2018
This comment has been minimized.
This comment has been minimized.
|
afaik this is now subsumed by #1053, and the status is still that we're waiting for specialization and other things to stabilize anyway |

kennytm commentedJan 14, 2015
Executive summary:
Rendered
cc #442, #290, rust-lang/rust#19032.