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

Closed
wants to merge 4 commits into
from

Conversation

Projects
None yet
@kennytm
Member

kennytm commented Jan 14, 2015

Executive summary:

impl<T: Int + !Float> Average for T { ... }
impl<T: Float + !Int> Average for T { ... }
impl<T: Int + Float> Average for T { ... }

Rendered

cc #442, #290, rust-lang/rust#19032.

@nikomatsakis nikomatsakis self-assigned this Jan 15, 2015

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Jan 15, 2015

Contributor

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.

Contributor

nikomatsakis commented Jan 15, 2015

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.

@blaenk

This comment has been minimized.

Show comment
Hide comment
@blaenk

blaenk Jan 16, 2015

Contributor

Very well done and comprehensive RFC 👏

Contributor

blaenk commented Jan 16, 2015

Very well done and comprehensive RFC 👏

@P1start

This comment has been minimized.

Show comment
Hide comment
@P1start

P1start Jan 16, 2015

Contributor

Syntax bikeshed: Use -, e.g., T: Int - Float, U: -Float. This is more consistent with +, but looks weird. (TBH, + is confusing in itself, especially because it’s usually associated with disjunction/union rather than conjunction/intersection, which is what it actually means.)

Contributor

P1start commented Jan 16, 2015

Syntax bikeshed: Use -, e.g., T: Int - Float, U: -Float. This is more consistent with +, but looks weird. (TBH, + is confusing in itself, especially because it’s usually associated with disjunction/union rather than conjunction/intersection, which is what it actually means.)

@kennytm

This comment has been minimized.

Show comment
Hide comment
@kennytm

kennytm Jan 16, 2015

Member

@P1start: Thanks, I've added T: -U to the alternatives section. I still prefer T: !U as (1) it looks more intuitive when used on its own, and (2) negative impls are already using !U.

Member

kennytm commented Jan 16, 2015

@P1start: Thanks, I've added T: -U to the alternatives section. I still prefer T: !U as (1) it looks more intuitive when used on its own, and (2) negative impls are already using !U.

@scialex

This comment has been minimized.

Show comment
Hide comment
@scialex

scialex Jan 16, 2015

Note: 'b: !'a is not the same as 'a: 'b. In 'b: !'a, the two lifetimes could merely be representing two irrelevant regions.

This seems to mean that 'b: !'a says that 'a is unrelated to 'b, in which case it seems to have no meaning over just 'b, 'a. Is this intentional?

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?

scialex commented Jan 16, 2015

Note: 'b: !'a is not the same as 'a: 'b. In 'b: !'a, the two lifetimes could merely be representing two irrelevant regions.

This seems to mean that 'b: !'a says that 'a is unrelated to 'b, in which case it seems to have no meaning over just 'b, 'a. Is this intentional?

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?

@kennytm

This comment has been minimized.

Show comment
Hide comment
@kennytm

kennytm Jan 16, 2015

Member

@scialex

This seems to mean that 'b: !'a says that 'a is unrelated to 'b, in which case it seems to have no meaning over just 'b, 'a. Is this intentional?

No. It guarantees 'b: 'a will not happen.

'a: 'b vs 'a: !'b

I included negatived lifetime bounds just for completion. I think the only useful example is to differentiate between 'static-types and non-'static-types.

impl Trait for &'static str { ... }
impl<'a: !'static> Trait for &'a str { ... }
Member

kennytm commented Jan 16, 2015

@scialex

This seems to mean that 'b: !'a says that 'a is unrelated to 'b, in which case it seems to have no meaning over just 'b, 'a. Is this intentional?

No. It guarantees 'b: 'a will not happen.

'a: 'b vs 'a: !'b

I included negatived lifetime bounds just for completion. I think the only useful example is to differentiate between 'static-types and non-'static-types.

impl Trait for &'static str { ... }
impl<'a: !'static> Trait for &'a str { ... }
@tomjakubowski

This comment has been minimized.

Show comment
Hide comment
@tomjakubowski

tomjakubowski Jan 17, 2015

Contributor

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 where T: Iterator, <T as Iterator>::Item != u8.

Contributor

tomjakubowski commented Jan 17, 2015

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 where T: Iterator, <T as Iterator>::Item != u8.

Show outdated Hide outdated text/0000-negative-bounds.md
### 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.

@P1start

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.

@P1start

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.

@kennytm

kennytm Jan 18, 2015

Member

Ah, thanks. Looks like I could remove this section then.

@kennytm

kennytm Jan 18, 2015

Member

Ah, thanks. Looks like I could remove this section then.

Removed T: !U for inequality bound since it is ambiguous for unsized …
…trait.

Added shorthand `T: Trait<Item != E>`.
@kennytm

This comment has been minimized.

Show comment
Hide comment
@kennytm

kennytm Jan 18, 2015

Member

@tomjakubowski : Yes it could work. Added it to the text too.

Member

kennytm commented Jan 18, 2015

@tomjakubowski : Yes it could work. Added it to the text too.

@theemathas

This comment has been minimized.

Show comment
Hide comment
@theemathas

theemathas 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 Foo implement A or B?

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 Foo implement A or B?

Show outdated Hide outdated text/0000-negative-bounds.md
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.

@reem

reem Jan 19, 2015

I understand what you mean in the second sentence here, but the language could be clarified.

@reem

reem Jan 19, 2015

I understand what you mean in the second sentence here, but the language could be clarified.

@kennytm

This comment has been minimized.

Show comment
Hide comment
@kennytm

kennytm Jan 20, 2015

Member

@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 Foo: A + B or Foo: !A + !B or undefined behavior or error.

Member

kennytm commented Jan 20, 2015

@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 Foo: A + B or Foo: !A + !B or undefined behavior or error.

@m13253

This comment has been minimized.

Show comment
Hide comment
@m13253

m13253 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.

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.

@kennytm

This comment has been minimized.

Show comment
Hide comment
@kennytm

kennytm Jan 31, 2015

Member

@m13253 You use (in)equality bounds for specific types:

impl<T> MyVec<T> where T != bool {}
impl MyVec<bool> {}
Member

kennytm commented Jan 31, 2015

@m13253 You use (in)equality bounds for specific types:

impl<T> MyVec<T> where T != bool {}
impl MyVec<bool> {}
@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Feb 2, 2015

Contributor

Gah. I haven't had time to really dig into this, but I have to and want to, because the inability to implement Fn traits widely is blocking some things I would really like to do -- and the only other alternative to negative bounds that I've found involves changes to coherence that effectively require the language to reconsider the correctness of every impl whenever any new impl is added, which seems like a non-starter (though I'm told this sort of exhaustive search is what Haskell does). In any case, this is all a long-winded way of something that I will make reading this in depth (and thinking about negative bounds) a priority today, which will hopefully lead to some more in-depth comments.

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.

Contributor

nikomatsakis commented Feb 2, 2015

Gah. I haven't had time to really dig into this, but I have to and want to, because the inability to implement Fn traits widely is blocking some things I would really like to do -- and the only other alternative to negative bounds that I've found involves changes to coherence that effectively require the language to reconsider the correctness of every impl whenever any new impl is added, which seems like a non-starter (though I'm told this sort of exhaustive search is what Haskell does). In any case, this is all a long-winded way of something that I will make reading this in depth (and thinking about negative bounds) a priority today, which will hopefully lead to some more in-depth comments.

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.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Feb 2, 2015

Contributor

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.)

Contributor

nikomatsakis commented Feb 2, 2015

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.)

@freebroccolo

This comment has been minimized.

Show comment
Hide comment
@freebroccolo

freebroccolo Feb 2, 2015

Contributor

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 where clauses…

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.

Contributor

freebroccolo commented Feb 2, 2015

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 where clauses…

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.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Feb 4, 2015

Contributor

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 specialization

It'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 implementation

Currently 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 call_foo will yield an ambiguity error, because the compiler won't be able to decide between those two impls. After all, it doesn't know that Float is implemented or not. You could work around this like so:

fn call_foo<T:Int+Foo>(t: T) { t.foo() }

At least as currently implemented, this would cause Foo to be resolved to the where clause instead of a concrete impl. This may seem like a silly distinction, but it's important for associated type resolution. T::Output in this case would normalize to T::Output -- or one could write T:Int+Foo<Output=i32> to manually specify the result.

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 Fn vs FnMut etc

My main motivation for pursuing something like negative trait bounds sooner rather than later is to attempt to resolve rust-lang/rust#18835. However, in working through this RFC, I came to the conclusion that negative trait bounds don't actually help there in particular. For reference, the problem is roughly that the bridging impls that interconvert from Fn to FnMut and so forth create coherence conflicts with many manual impls of the Fn traits. (As explained in rust-lang/rust#19032, the problem is not really specific to the Fn traits, but they happen to be a relatively urgent manifestation of it.)

The problem. To simplify the situation, let's just consider Fn and FnMut. The story begins with the bridging impl from Fn to FnMut:

impl<A,F> FnMut<A> for F
    where F : Fn<A>
{
    type Output = F::Output;
} // Impl 1.

Now, imagine I try to write an impl of FnMut for some custom type of my own:

struct MyType<A> { ... }
impl<A> FnMut<A> for MyType<A> { ... } // Impl 2.

This impl is considered to overlap with the bridge impl above. That may seem a bit surprising. But in fact it's not wrong. After all, according to the rules as currently written, it is possible for some other crate to come along extend MyType with another impl:

struct TheirArgument;
impl Fn<(TheirArgument,)> for MyType<TheirArgument> { ... } // Impl 3.

Now the problem is that given these three impls, if we have the trait obligation MyType<TheirArgument> : FnMut<(TheirArgument,)>, it can be implemented in two ways. The bridge impl (Impl 1) combined with Impl 3, and the other Impl (Impl 2).

Another impl I would like to write is:

trait FnBox<A> { ... }
impl<A,F> FnOnce<A> for Box<F>
    where F : FnBox<A>
{
    ...
}

This falls afoul of coherence as well, for similar reasons.

The solutions. There are a couple of ways out of this problem.

A. Adjust the coherence rules. It is certainly conceivable that we could adjust the current overlap check to permit Impl 2 but reject Impl 3. However, this is a mite tricky, because it requires that whenever a downstream crate adds any impl we must recheck (potentially) all impls for overlap. E.g. in this case, Impl 3 is an impl of the Fn trait, but the conflict that it induces is between Impls 1 and 3, both impls of the FnMut trait. (Incidentally, I'm told that this kind of check is precisely what Haskell does to ensure non-overlap for type families, though I couldn't find a good citation here.)

B. Negative trait bounds on Impl 2. We could consider employing negative trait bounds in a variety of ways. One might be to attach a negative trait bound to Impl 2:

struct MyType<A> { ... }
impl<A> FnMut<A> for MyType<A>
    where MyType<A> : !Fn<A>
{ ... } // Impl 2b.

This would allow the coherence checker to accept Impl 2 (which is currently rejected). Interestingly, it does not disallow Impl 3, it merely gives it precedences over Impl 2. This is perhaps not the outcome we wanted. Another problem with this solution is that it interacts with the need for disambiguation in the type checker, so that if we were to write a generic function like:

fn foo<A>(m: MyType<A>, arg: A) { m(a); }

This function would not type-check, because Impl 2 is not known to apply. We'd have to write:

fn foo<A>(m: MyType<A>, arg: A) where MyType<A> : !Fn<A> { m(a); }

(Incidentally, this makes sense because it's possible that the return type specified in impl 2 and the return type in some hypothetical impl 3 don't agree.)

This drawback effectively rules out this solution, at least in isolation.

C. Negative trait bounds on the FnMut trait. The intention of the bridging impls, really, is that you will implement at most one of the various Fn traits for a given type. So it might be interesting to try and somehow declare Fn and FnMut disjoint. For example maybe like this:

trait FnMut<A> : !Fn<A> { ... }

This sort of makes sense, in that if you implement FnMut, you should not implement Fn. But of course it also doesn't make sense, because then the bridge impl that says "here is an automatic impl of FnMut if you implement Fn" seems to be a violation.

Maybe the other way is good?

trait Fn<A> : !FnMut<A> { ... }

But this is also not right. After all, every type that implements Fn implements FnMut. All in all this idea is merely flawed. The idea is in fact not that every type will implement at most one of the Fn traits, but rather that they will implement at most one directly.

An aside: Negative trait bounds in supertypes are also interesting because they are semantically quite different from positive bounds. In particular, a positive supertrait bound is (today) verified at the impl site. But a negative supetrait bound cannot necessarily be verified purely at the impl site, it must also be verified in downstream crates. This seems to raise a very similar probelm to option A above, where every impl must be checked against all upstream impls to see whether it happens to invalidate them. This should perhaps not be surprising, since a negative supertrait is effectively making a more explicit form of the same sort of constraint that was implied in Option A.

D. Supertraits. One way to sidestep this problem is to dump the bridging impls and use supertraits instead:

trait Fn<A> : FnMut<A> { fn call(&self) -> Self::Output; }
trait FnMut<A> : FnOnce<A> { fn call_mut(&mut self) -> Self::Output; }
trait FnOnce<A> { type Output; fn call_once(self) -> Self::Output; }

The fact that this makes sense should make it clear that the negative supertrait bound from Option C was wrong-headed. ;) There are however several downsides to Option D:

  • To implement Fn, you must write your own bridging impls of FnMut and FnOnce.
  • The semantics of Fn, FnMut, and FnOnce can diverge.

All in all supertraits don't feel as good. And of course it leaves the crucial problem of permitting convenient bridging impls unsolved; there are other cases where a "partial bridge" feels right, after all.

Conclusions

Not a lot of conclusions just now, except that I want to find a solution for the Fn traits but don't yet know what I think it ought to be.

Contributor

nikomatsakis commented Feb 4, 2015

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 specialization

It'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 implementation

Currently 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 call_foo will yield an ambiguity error, because the compiler won't be able to decide between those two impls. After all, it doesn't know that Float is implemented or not. You could work around this like so:

fn call_foo<T:Int+Foo>(t: T) { t.foo() }

At least as currently implemented, this would cause Foo to be resolved to the where clause instead of a concrete impl. This may seem like a silly distinction, but it's important for associated type resolution. T::Output in this case would normalize to T::Output -- or one could write T:Int+Foo<Output=i32> to manually specify the result.

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 Fn vs FnMut etc

My main motivation for pursuing something like negative trait bounds sooner rather than later is to attempt to resolve rust-lang/rust#18835. However, in working through this RFC, I came to the conclusion that negative trait bounds don't actually help there in particular. For reference, the problem is roughly that the bridging impls that interconvert from Fn to FnMut and so forth create coherence conflicts with many manual impls of the Fn traits. (As explained in rust-lang/rust#19032, the problem is not really specific to the Fn traits, but they happen to be a relatively urgent manifestation of it.)

The problem. To simplify the situation, let's just consider Fn and FnMut. The story begins with the bridging impl from Fn to FnMut:

impl<A,F> FnMut<A> for F
    where F : Fn<A>
{
    type Output = F::Output;
} // Impl 1.

Now, imagine I try to write an impl of FnMut for some custom type of my own:

struct MyType<A> { ... }
impl<A> FnMut<A> for MyType<A> { ... } // Impl 2.

This impl is considered to overlap with the bridge impl above. That may seem a bit surprising. But in fact it's not wrong. After all, according to the rules as currently written, it is possible for some other crate to come along extend MyType with another impl:

struct TheirArgument;
impl Fn<(TheirArgument,)> for MyType<TheirArgument> { ... } // Impl 3.

Now the problem is that given these three impls, if we have the trait obligation MyType<TheirArgument> : FnMut<(TheirArgument,)>, it can be implemented in two ways. The bridge impl (Impl 1) combined with Impl 3, and the other Impl (Impl 2).

Another impl I would like to write is:

trait FnBox<A> { ... }
impl<A,F> FnOnce<A> for Box<F>
    where F : FnBox<A>
{
    ...
}

This falls afoul of coherence as well, for similar reasons.

The solutions. There are a couple of ways out of this problem.

A. Adjust the coherence rules. It is certainly conceivable that we could adjust the current overlap check to permit Impl 2 but reject Impl 3. However, this is a mite tricky, because it requires that whenever a downstream crate adds any impl we must recheck (potentially) all impls for overlap. E.g. in this case, Impl 3 is an impl of the Fn trait, but the conflict that it induces is between Impls 1 and 3, both impls of the FnMut trait. (Incidentally, I'm told that this kind of check is precisely what Haskell does to ensure non-overlap for type families, though I couldn't find a good citation here.)

B. Negative trait bounds on Impl 2. We could consider employing negative trait bounds in a variety of ways. One might be to attach a negative trait bound to Impl 2:

struct MyType<A> { ... }
impl<A> FnMut<A> for MyType<A>
    where MyType<A> : !Fn<A>
{ ... } // Impl 2b.

This would allow the coherence checker to accept Impl 2 (which is currently rejected). Interestingly, it does not disallow Impl 3, it merely gives it precedences over Impl 2. This is perhaps not the outcome we wanted. Another problem with this solution is that it interacts with the need for disambiguation in the type checker, so that if we were to write a generic function like:

fn foo<A>(m: MyType<A>, arg: A) { m(a); }

This function would not type-check, because Impl 2 is not known to apply. We'd have to write:

fn foo<A>(m: MyType<A>, arg: A) where MyType<A> : !Fn<A> { m(a); }

(Incidentally, this makes sense because it's possible that the return type specified in impl 2 and the return type in some hypothetical impl 3 don't agree.)

This drawback effectively rules out this solution, at least in isolation.

C. Negative trait bounds on the FnMut trait. The intention of the bridging impls, really, is that you will implement at most one of the various Fn traits for a given type. So it might be interesting to try and somehow declare Fn and FnMut disjoint. For example maybe like this:

trait FnMut<A> : !Fn<A> { ... }

This sort of makes sense, in that if you implement FnMut, you should not implement Fn. But of course it also doesn't make sense, because then the bridge impl that says "here is an automatic impl of FnMut if you implement Fn" seems to be a violation.

Maybe the other way is good?

trait Fn<A> : !FnMut<A> { ... }

But this is also not right. After all, every type that implements Fn implements FnMut. All in all this idea is merely flawed. The idea is in fact not that every type will implement at most one of the Fn traits, but rather that they will implement at most one directly.

An aside: Negative trait bounds in supertypes are also interesting because they are semantically quite different from positive bounds. In particular, a positive supertrait bound is (today) verified at the impl site. But a negative supetrait bound cannot necessarily be verified purely at the impl site, it must also be verified in downstream crates. This seems to raise a very similar probelm to option A above, where every impl must be checked against all upstream impls to see whether it happens to invalidate them. This should perhaps not be surprising, since a negative supertrait is effectively making a more explicit form of the same sort of constraint that was implied in Option A.

D. Supertraits. One way to sidestep this problem is to dump the bridging impls and use supertraits instead:

trait Fn<A> : FnMut<A> { fn call(&self) -> Self::Output; }
trait FnMut<A> : FnOnce<A> { fn call_mut(&mut self) -> Self::Output; }
trait FnOnce<A> { type Output; fn call_once(self) -> Self::Output; }

The fact that this makes sense should make it clear that the negative supertrait bound from Option C was wrong-headed. ;) There are however several downsides to Option D:

  • To implement Fn, you must write your own bridging impls of FnMut and FnOnce.
  • The semantics of Fn, FnMut, and FnOnce can diverge.

All in all supertraits don't feel as good. And of course it leaves the crucial problem of permitting convenient bridging impls unsolved; there are other cases where a "partial bridge" feels right, after all.

Conclusions

Not a lot of conclusions just now, except that I want to find a solution for the Fn traits but don't yet know what I think it ought to be.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Feb 4, 2015

Contributor

@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.

Contributor

nikomatsakis commented Feb 4, 2015

@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.

@kennytm

This comment has been minimized.

Show comment
Hide comment
@kennytm

kennytm Feb 4, 2015

Member

@nikomatsakis Thanks for the comment 😄 About the Fn vs FnMut section,

  • Option D is actually what is happening right now with PartialOrd vs Ord. Perhaps there could be a facility that a subtrait could provide default and non-overridable impl to supertraits' methods.

    trait Fn<A> : FnMut<A> {
        final fn call_mut(&mut self, args: A) -> Self::Output { self.call(args) }
        fn call(&self, args: A) -> Self::Output;
    }

    However, this only tackles the case where there is already a trait hierarchy. This does not solve the issue where one wants blanket impls for two unrelated traits (#442). BTW, we cannot let trait FnMut<A> : FnOnce<A>, because FnMut allows ?Sized self, but FnOnce requires Sized. And to supply the associated type Output to Fn/FnMut, we can only do it through FnOnce, which feels awkward.

  • I thought about Option E: struct MyType<A> where MyType<A>: !Fn<A>, which currently it is "illegal recursive type". If there was no error, this could prevent impl 3, but I'm not sure if fn foo<A>(m: MyType<A>, arg: A) { m(a); } still needs the where clause to work.

Member

kennytm commented Feb 4, 2015

@nikomatsakis Thanks for the comment 😄 About the Fn vs FnMut section,

  • Option D is actually what is happening right now with PartialOrd vs Ord. Perhaps there could be a facility that a subtrait could provide default and non-overridable impl to supertraits' methods.

    trait Fn<A> : FnMut<A> {
        final fn call_mut(&mut self, args: A) -> Self::Output { self.call(args) }
        fn call(&self, args: A) -> Self::Output;
    }

    However, this only tackles the case where there is already a trait hierarchy. This does not solve the issue where one wants blanket impls for two unrelated traits (#442). BTW, we cannot let trait FnMut<A> : FnOnce<A>, because FnMut allows ?Sized self, but FnOnce requires Sized. And to supply the associated type Output to Fn/FnMut, we can only do it through FnOnce, which feels awkward.

  • I thought about Option E: struct MyType<A> where MyType<A>: !Fn<A>, which currently it is "illegal recursive type". If there was no error, this could prevent impl 3, but I'm not sure if fn foo<A>(m: MyType<A>, arg: A) { m(a); } still needs the where clause to work.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Feb 4, 2015

Contributor

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 T:Int and the T:Float+!Int impl (or whatever), it would still have to be totally sure that at least one of them will hold. First-class specialization again provides us a kind of answer here, because if we see a "low-priority" impl that we can completely match, that ensures us that at least this low-priority impl will match, even though there may be higher-priority ones that are better (depending on the details of what we decide, it might also tell us to avoid relying on the specific associated types in the type checker, since the impl is only low priority and hence could be overridden).

Contributor

nikomatsakis commented Feb 4, 2015

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 T:Int and the T:Float+!Int impl (or whatever), it would still have to be totally sure that at least one of them will hold. First-class specialization again provides us a kind of answer here, because if we see a "low-priority" impl that we can completely match, that ensures us that at least this low-priority impl will match, even though there may be higher-priority ones that are better (depending on the details of what we decide, it might also tell us to avoid relying on the specific associated types in the type checker, since the impl is only low priority and hence could be overridden).

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Feb 4, 2015

Contributor

@kennytm yes, a very good point about FnMut and FnOnce. Actually, the problem is not so much that FnOnce requires Sized (I don't think it does today -- it's just that Sized is required to call call_once), but that FnOnce is not object safe. And yes I agree that the Ord : PartialOrd and Eq : PartialEq situation is quite analogous.

Ignoring the problem with Sized (I'll get to that in a second...), I was also wondering if a supertrait relationship is the right thing here. In some sense, it feels semantically right, but it's just that there is a missing feature, which is a more convenient way to specify default implementations that would make supertraits more ergonomic. The disappointing facet is that we don't address interconversion between distinct traits, as you point out, nor perhaps "conditional" interconversion (not everything that is A is X, but everything that is (A+B) is X). But maybe those cases come later (and maybe conditional cases can be handled by making a subtrait C = A+B?).

@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 Self : Sized are exempt from the object-safety rules. The reasoning is that you could never call them with Self=Trait, either in a generic or other setting, because that would fail the where-clause. This seems like a really nice solution to the occasional limitations imposed by object safety (like this one).

Regarding your proposed Option E (struct MyType<A> where MyType<A> : !Fn<A>), that's interesting. I felt like there was another option I wasn't seeing. (Specifically I was wanting to say somehow when declaring MyType how it was intended to work, so that's good.) (Btw, the "illegal recursive type" thing is a red herring -- just a bug.)

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 MyType<A> type reference is "well-formed" (meets all its declared constraints). But it feels pretty silly. My implied bounds blog post seems pretty relevant, since that would make it so that the fn can infer that MyType<A> : !Fn<A> holds based on the struct definition. (I've actually been planning to experiment with implementing implied bounds sooner rather than later for other reasons, we'll see how that goes.) Depending on the details of the proposal, the impl might still require the where clause, but that's probably ok.

Contributor

nikomatsakis commented Feb 4, 2015

@kennytm yes, a very good point about FnMut and FnOnce. Actually, the problem is not so much that FnOnce requires Sized (I don't think it does today -- it's just that Sized is required to call call_once), but that FnOnce is not object safe. And yes I agree that the Ord : PartialOrd and Eq : PartialEq situation is quite analogous.

Ignoring the problem with Sized (I'll get to that in a second...), I was also wondering if a supertrait relationship is the right thing here. In some sense, it feels semantically right, but it's just that there is a missing feature, which is a more convenient way to specify default implementations that would make supertraits more ergonomic. The disappointing facet is that we don't address interconversion between distinct traits, as you point out, nor perhaps "conditional" interconversion (not everything that is A is X, but everything that is (A+B) is X). But maybe those cases come later (and maybe conditional cases can be handled by making a subtrait C = A+B?).

@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 Self : Sized are exempt from the object-safety rules. The reasoning is that you could never call them with Self=Trait, either in a generic or other setting, because that would fail the where-clause. This seems like a really nice solution to the occasional limitations imposed by object safety (like this one).

Regarding your proposed Option E (struct MyType<A> where MyType<A> : !Fn<A>), that's interesting. I felt like there was another option I wasn't seeing. (Specifically I was wanting to say somehow when declaring MyType how it was intended to work, so that's good.) (Btw, the "illegal recursive type" thing is a red herring -- just a bug.)

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 MyType<A> type reference is "well-formed" (meets all its declared constraints). But it feels pretty silly. My implied bounds blog post seems pretty relevant, since that would make it so that the fn can infer that MyType<A> : !Fn<A> holds based on the struct definition. (I've actually been planning to experiment with implementing implied bounds sooner rather than later for other reasons, we'll see how that goes.) Depending on the details of the proposal, the impl might still require the where clause, but that's probably ok.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Feb 4, 2015

Contributor

I guess besides ergonomics, the shortcoming of "inheritance" for Fn vs FnMut is the inability to guarantee semantic equivalence between them (similar to Ord and PartialOrd). This may be more of a theoretical than a practical concern, though I'm sure some practical joker will make some twisted type where it does something different when called with FnMut than Fn.

Contributor

nikomatsakis commented Feb 4, 2015

I guess besides ergonomics, the shortcoming of "inheritance" for Fn vs FnMut is the inability to guarantee semantic equivalence between them (similar to Ord and PartialOrd). This may be more of a theoretical than a practical concern, though I'm sure some practical joker will make some twisted type where it does something different when called with FnMut than Fn.

@glaebhoerl

This comment has been minimized.

Show comment
Hide comment
@glaebhoerl

glaebhoerl Feb 4, 2015

Contributor

I guess besides ergonomics, the shortcoming of "inheritance" for Fn vs FnMut is the inability to guarantee semantic equivalence between them (similar to Ord and PartialOrd).

My feeling is that we should have both inheritance and forwarding impls (it feels wrong for the Fn, FnMut, and FnOnce traits to not be formally connected to each other except through impls), though this is "only" a gut feeling and I don't have a sense of how it would bear on the practical difficulties at issue here - maybe it would be the worst of both worlds (although my inclination would be to read that as a symptom of something else having gone awry somewhere).

(A more out-there idea is that if we had the ability to parameterize over the capabilities of references - shared, mut, move, etc. - then the three Fn* traits could hopefully be collapsed into one, and perhaps the whole problem would just go away. But this is unlikely to be on the table before 1.0, to put it mildly.)

struct TheirArgument;
impl Fn<(TheirArgument,)> for MyType<TheirArgument> { ... } // Impl 3.

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.

Contributor

glaebhoerl commented Feb 4, 2015

I guess besides ergonomics, the shortcoming of "inheritance" for Fn vs FnMut is the inability to guarantee semantic equivalence between them (similar to Ord and PartialOrd).

My feeling is that we should have both inheritance and forwarding impls (it feels wrong for the Fn, FnMut, and FnOnce traits to not be formally connected to each other except through impls), though this is "only" a gut feeling and I don't have a sense of how it would bear on the practical difficulties at issue here - maybe it would be the worst of both worlds (although my inclination would be to read that as a symptom of something else having gone awry somewhere).

(A more out-there idea is that if we had the ability to parameterize over the capabilities of references - shared, mut, move, etc. - then the three Fn* traits could hopefully be collapsed into one, and perhaps the whole problem would just go away. But this is unlikely to be on the table before 1.0, to put it mildly.)

struct TheirArgument;
impl Fn<(TheirArgument,)> for MyType<TheirArgument> { ... } // Impl 3.

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.

@m13253

This comment has been minimized.

Show comment
Hide comment
@m13253

m13253 Feb 7, 2015

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 call_foo will yield an ambiguity error, because the compiler won't be able to decide between those two impls. After all, it doesn't know that Float is implemented or not.

  1. Why the compiler should decide which impl to use before a generic function is used? I don't think the compiler should generate object code if call_foo is not used. And When call_foo is used, which impl to use is clear then.
  2. I think type hint (::<T>) should always be available as last resort when the compiler can't infer which impl to use.

Or maybe I'm not getting the point?

m13253 commented Feb 7, 2015

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 call_foo will yield an ambiguity error, because the compiler won't be able to decide between those two impls. After all, it doesn't know that Float is implemented or not.

  1. Why the compiler should decide which impl to use before a generic function is used? I don't think the compiler should generate object code if call_foo is not used. And When call_foo is used, which impl to use is clear then.
  2. I think type hint (::<T>) should always be available as last resort when the compiler can't infer which impl to use.

Or maybe I'm not getting the point?

@bombless

This comment has been minimized.

Show comment
Hide comment
@bombless

bombless 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.
That's how Rust's generics works.

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.
That's how Rust's generics works.

@bluss

This comment has been minimized.

Show comment
Hide comment
@bluss

bluss 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.

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.

@kennytm

This comment has been minimized.

Show comment
Hide comment
@kennytm

kennytm Feb 11, 2015

Member

@bluss: I don't quite understand. I believe a similar issue already happens with default impl + negative impl?

Member

kennytm commented Feb 11, 2015

@bluss: I don't quite understand. I believe a similar issue already happens with default impl + negative impl?

@bluss

This comment has been minimized.

Show comment
Hide comment
@bluss

bluss 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 Range<i32> impls Debug, but Chunks doesn't. With negative trait bounds in for example Rust 1.0, we can't add an impl of Debug to Chunks in Rust 1.1, because it would be backwards incompatible; it would change the behavior of Rust programs.

// 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 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 Range<i32> impls Debug, but Chunks doesn't. With negative trait bounds in for example Rust 1.0, we can't add an impl of Debug to Chunks in Rust 1.1, because it would be backwards incompatible; it would change the behavior of Rust programs.

// 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();
}
@kennytm

This comment has been minimized.

Show comment
Hide comment
@kennytm

kennytm Feb 11, 2015

Member

@bluss I don't find this convincing, as the example is using IsShow::number() without using any aspect of the Debug trait. It is just like one is relying on size_of::<Foo>() on something unrelated to the stated purpose, and then say the size_of is broken when Foo's internal representation is changed.

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)
    }
}
Member

kennytm commented Feb 11, 2015

@bluss I don't find this convincing, as the example is using IsShow::number() without using any aspect of the Debug trait. It is just like one is relying on size_of::<Foo>() on something unrelated to the stated purpose, and then say the size_of is broken when Foo's internal representation is changed.

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)
    }
}
@bluss

This comment has been minimized.

Show comment
Hide comment
@bluss

bluss 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.

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.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Apr 6, 2015

Contributor

@bluss indeed, it looks like you and I were exploring similar thoughts in parallel.

Contributor

nikomatsakis commented Apr 6, 2015

@bluss indeed, it looks like you and I were exploring similar thoughts in parallel.

@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Apr 6, 2015

Contributor

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:

  1. We adjusted the Fn traits to use inheritance. This unblocks the immediate problem of not being able to write impls for them but has an ergonomic hit in that one must implement multiple traits. I'm inclined to agree with @glaebhoerl that the ideal setup is to have both inheritance and bridging impls, but there are complications (in particular, the Output associated type is defined in FnOnce, so it is difficult to write a bridging impl from Fn to FnOnce as we had in the past, since the value of Output must be inferred from the return type of the call fn).
  2. I wrote up a detailed analysis of the perils of negative reasoning for forwards compatibility, and proposed several measures to mitigate those fears. The proposal has since been accepted and largely implemented. Part of that proposal is that we now apply (modified) orphan rules to all uses of negative reasoning. These rules should be incorporated into this RFC (or any follow-up RFC that aims to make negative reasoning first-class).
  3. In that same analysis, I also pointed out that the OIBIT proposal in fact already permits negative bounds to be expressed, albeit in a roundabout way, and proposed some restrictions to change that. These have not yet been implemented, but negative impls are currently feature-gated.
  4. On a more personal note, I have come to believe that some form of first-class specialization (i.e., not based on using negative bounds to ensure coherence, but rather permitting overlap) is basically necessary for a number of reasons. I intend to be drafting proposals along these lines in the not too distant future (and anyone who is interested should feel free to ping me on IRC...). There is obviously a lot of overlap between explicit negative bounds and specialization, though I think each has something to offer that the other cannot, at least in some scenarios. It'd be good to examine those cases where negative bounds give more power.
Contributor

nikomatsakis commented Apr 6, 2015

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:

  1. We adjusted the Fn traits to use inheritance. This unblocks the immediate problem of not being able to write impls for them but has an ergonomic hit in that one must implement multiple traits. I'm inclined to agree with @glaebhoerl that the ideal setup is to have both inheritance and bridging impls, but there are complications (in particular, the Output associated type is defined in FnOnce, so it is difficult to write a bridging impl from Fn to FnOnce as we had in the past, since the value of Output must be inferred from the return type of the call fn).
  2. I wrote up a detailed analysis of the perils of negative reasoning for forwards compatibility, and proposed several measures to mitigate those fears. The proposal has since been accepted and largely implemented. Part of that proposal is that we now apply (modified) orphan rules to all uses of negative reasoning. These rules should be incorporated into this RFC (or any follow-up RFC that aims to make negative reasoning first-class).
  3. In that same analysis, I also pointed out that the OIBIT proposal in fact already permits negative bounds to be expressed, albeit in a roundabout way, and proposed some restrictions to change that. These have not yet been implemented, but negative impls are currently feature-gated.
  4. On a more personal note, I have come to believe that some form of first-class specialization (i.e., not based on using negative bounds to ensure coherence, but rather permitting overlap) is basically necessary for a number of reasons. I intend to be drafting proposals along these lines in the not too distant future (and anyone who is interested should feel free to ping me on IRC...). There is obviously a lot of overlap between explicit negative bounds and specialization, though I think each has something to offer that the other cannot, at least in some scenarios. It'd be good to examine those cases where negative bounds give more power.
@nikomatsakis

This comment has been minimized.

Show comment
Hide comment
@nikomatsakis

nikomatsakis Apr 10, 2015

Contributor

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.

Contributor

nikomatsakis commented Apr 10, 2015

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment