Skip to content

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

Open

Description

@udoprog

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;
}

Activity

  1. added
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    on Aug 6, 2021
  2. udoprog commented on Aug 7, 2021

    @udoprog
    ContributorAuthor

    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;
    }
  3. jackh726 commented on Aug 10, 2021

    @jackh726
    Member

    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.

  4. jackh726 commented on Oct 16, 2021

    @jackh726
    Member

    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.

  5. added a commit that references this issue on Sep 13, 2022
  6. added a commit that references this issue on Jan 24, 2023
  7. added
    A-GATsArea: Generic associated types (GATs)
    and removed
    C-bugCategory: This is a bug.
    on Sep 24, 2024
  8. added
    C-discussionCategory: Discussion or questions that doesn't represent real issues.
    T-typesRelevant to the types team, which will review and decide on the PR/issue.
    on Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-GATsArea: Generic associated types (GATs)C-discussionCategory: Discussion or questions that doesn't represent real issues.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-typesRelevant to the types team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions