rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
97.53k stars 12.61k forks source link

Inconsistent treatment of different kinds of trait bounds with GATs #87831

Open udoprog opened 3 years ago

udoprog commented 3 years ago

I tried this code:

#![feature(generic_associated_types)]

trait Collection {
    type Iter<'a>: IntoIterator
    where
        <Self::Iter<'a> as IntoIterator>::Item: std::fmt::Debug;
}

This fails to compile with:

   Compiling playground v0.0.1 (/playground)
error[E0277]: the trait bound `<Self as Collection>::Iter<'a>: IntoIterator` is not satisfied
 --> src/lib.rs:4:5
  |
4 | /     type Iter<'a>: IntoIterator
5 | |     where
6 | |         <Self::Iter<'a> as IntoIterator>::Item: std::fmt::Debug;
  | |________________________________________________________________^ the trait `IntoIterator` is not implemented for `<Self as Collection>::Iter<'a>`
  |
help: consider further restricting the associated type
  |
3 | trait Collection where <Self as Collection>::Iter<'a>: IntoIterator {
  |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error[E0277]: the trait bound `<Self as Collection>::Iter<'a>: IntoIterator` is not satisfied
   --> src/lib.rs:4:20
    |
4   |       type Iter<'a>: IntoIterator
    |                      ^^^^^^^^^^^^ the trait `IntoIterator` is not implemented for `<Self as Collection>::Iter<'a>`
    |
help: consider further restricting the associated type
    |
3   | trait Collection where <Self as Collection>::Iter<'a>: IntoIterator {
    |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground` due to 2 previous errors

Playground

aka/rrevenantt on Discord discovered the following workaround, which to me seems like it should be treated the same as above. The only difference is the location of the bound - one using "inheritance style", the other as part of a where clause:

#![feature(generic_associated_types)]

trait Collection {
    type Iter<'a>
    where
        Self::Iter<'a>: IntoIterator,
        <Self::Iter<'a> as IntoIterator>::Item: std::fmt::Debug;
}

It's worth noting that the same issue exists without GATs. But where clauses on associated types are also feature gated under generic_associated_types.

This fails to compile:

#![feature(generic_associated_types)]

trait Collection {
    type Iter: IntoIterator
    where
        <Self::Iter as IntoIterator>::Item: std::fmt::Debug;
}

However this works on stable (lifting the bound to the trait itself):

trait Collection
where
    <Self::Iter as IntoIterator>::Item: std::fmt::Debug,
{
    type Iter: IntoIterator;
}
udoprog commented 3 years ago

So I encountered that the proposed workaround might not be as workable as I'd like, in that it requires the caller to specify that bound at each item where the associated type is used, like so:

#![feature(generic_associated_types)]

trait Collection {
    type Iter
    where
        Self::Iter: IntoIterator,
        <Self::Iter as IntoIterator>::Item: std::fmt::Debug;

    fn iter(&self) -> Self::Iter
    where
        Self::Iter: IntoIterator,
        <Self::Iter as IntoIterator>::Item: std::fmt::Debug;
}

And in my mental model I think this makes sense in general with GATs, because the item (fn iter) might specify additional bounds on generic parameters. But it also means that it's a difference (intentional or not) with inheritance-style bounds like type Iter<'a>: IntoIterator which does not need to be specified every time it's used.

Like this:

#![feature(generic_associated_types)]

trait Collection {
    type Iter<'a>: IntoIterator;

    fn iter(&self) -> Self::Iter<'_>;
}

Compared to this:

#![feature(generic_associated_types)]

trait Collection {
    type Iter<'a>
    where
        Self::Iter<'a>: IntoIterator;

    fn iter<'a>(&'a self) -> Self::Iter<'a>
    where
        Self::Iter<'a>: IntoIterator;
}
jackh726 commented 3 years ago

Ah, so. There indeed is a difference between writing

trait Foo {
  type Bar<T>: Debug;
}

and

trait Foo {
  type Bar<T> where Self::Bar<T>: Debug;
}

Specifically, the bounds type Bar<T>: Debug must be proved by the impl, whereas the where clauses must be provable by the type.

The well-formedness rules for these are generally laid out pretty well in the Chalk book (http://rust-lang.github.io/chalk/book/clauses/wf.html). Though, this is the "ideal" world and might not be completely implemented in rustc this way (these in some ways rely on implied bounds and such).

But, I wanted to point out that for

trait Trait<P1...> where WC_trait {
    type Assoc<P2...>: Bounds_assoc where WC_assoc;
}

we generate the following goal:

forall<P1...> {
    if (FromEnv(WC_trait)) {
        WellFormed(InputTypes(WC_trait)) &&

            forall<P2...> {
                if (FromEnv(WC_assoc)) {
                    WellFormed(InputTypes(Bounds_assoc)) &&
                        WellFormed(InputTypes(WC_assoc))
                }
            }
    }
}

This is essentially saying that "if we assume the where clauses on the trait hold, then we require that the types present on those where clauses be well formed" and "if we assume the where clauses on the associated type hold too, then we require that the types in the bounds and where clauses on the associated type are well formed"

For this example:

trait Collection {
    type Iter<'a>: IntoIterator
    where
        <Self::Iter<'a> as IntoIterator>::Item: std::fmt::Debug;
}

there are no where clauses on the trait, so that bit is straightforward. But then we say that if <Self::Iter<'a> as IntoIterator>::Item: std::fmt::Debug holds then we require that Self::Iter<'a>: IntoIterator is well-formed. Importantly, the rules as-written don't take into account the Iter<'a>: IntoIterator bound.

At first thought, this feels wrong to me: given that we have the IntoIterator bound, should we always satisfy that (and so we don't need to repeat that in the where clause)? Not sure :) This is something to think about.

jackh726 commented 2 years ago

GATs issue triage: not blocking. See my previous comment. This is probably code that shouldn't be written as-is. But nonetheless is not really a GATs issue.