Spoilers: I do a long detour through GATs that doesn't matter, and then wrap up with a more-verbose-to-implement but also more-ergonomic-to-read alternative at the end. Skip to the end if you just want an alternative answer to the OP and not a long winded "GATs don't really help." (CC @pandamatic)
There is a general where
for GATs but we have to be sort of careful and technical about what that means.
trait Foo {
type Bar<T: Grok>: Hack where T: Ick;
}
Here, Grok
and Ick
are both where
-clauses. They apply to the inputs of the GAT and are enforced at the point of normalization, and the implementation of Foo
can rely on them holding.
But Hack
is a bound on the GAT itself, just like we can have bounds directly on (plain) associated types today. The meaning is the same -- those bounds are translated to where clauses on the trait.
// Made up syntax
trait Foo where for<T> <Self as Foo>::Bar<T>: Hack { ... }
These have to be proven by the implementation.
Alright, back to our situation. We want a more ergonomic way to express the "where clause on trait" variety of bound that we already have, but for something too complicated to apply directly to the associated type. Currently, the other way to express bounds are where
clauses. But where
clauses on associated items like GATs (or functions) are semantically different.
Does the difference matter? I.e. can we give up on the "behaves exactly the same" but still get the bounds we want using where
clauses?
I'm not aware of a way to do so yet, so I can't even experiment and see. With GATs, we can spell out a path to itself in a where
clause:
trait MyTrait {
type Bar<T>: LibraryTrait
where <Self::Bar<T> as LibraryTrait>::Foo: Clone;
}
That even compiles... on its own. But if you try to actually use it, you trigger E0275: Recursive trait requirement.
Even if this was special-cased to be a where
clause on the trait and not on the associated type or GAT, it's not much of an ergonomic improvement. We've just moved it around. That said, special-casing it may be considered reasonable, analogous to this code which is valid today:
trait Foo: Super {}
trait Foo where Self: Super {} // same thing
So, to summarize, I don't think GATs as they are implemented today gain us much. They could be updated to gain us a little, but just a little. All out of luck without an RFC for new syntax then?
No, there's another alternative: Associated type bounds (tracking issue).
trait MyTrait {
type Bar: LibraryTrait<Foo: Clone>;
}
Sadly, I doubt it's close to stabilization. This case doesn't work on nightly.
Taking inspiration from that RFC though, here's an alternative that's stable today:
trait MyTrait {
type Foo: Clone;
type Bar: LibraryTrait<Foo = Self::Foo>;
}
It's more verbose to use, but certainly easier to read. I probably should have thought of this earlier -- it's the pattern IntoIterator uses.