Constrained is right term, it just rarely comes up. Whereas the constraints imposed by trait bounds and lifetime relations are ever-present, and you used the word "constraint" specifically on the first go .
Well, let's take a look. Here's an example from the second post, modified to compile on nightly. It doesn't look great due to the lack of implied bounds, but anyway, it works. We could implement the collection family pattern for LinkedList
and VecDeque
and whatever else, and we could then floatify
them. Now let's see if we can get rid of the GATs -- it has two, a type GAT in CollectionFamily
and a lifetime GAT in Collection<_>
.
There actually is a reasonably well-known pattern for emulating lifetime GATs on stable. I've applied it here, and though it has added some noise, everything still works. Part of why it works is that supertrait bounds are implied, so when you know something implements Collection<Item>
, you can just assume they implement IterType<'any, Item>
too. That's what the for<'a> ...
supertrait bound is giving us. Great, half-way there.
Now let's apply that pattern to CollectionFamily
and uh... oh, dang it.
error: only lifetime parameters can be used in this context
--> src/lib.rs:5:33
|
5 | pub trait CollectionFamily: for<T> MemberType<T> {}
| ^
Higher-ranked bounds (for<'any> ...
) only work with lifetimes, not types.
-pub trait CollectionFamily: for<T> MemberType<T> {}
pub trait Collection<Item>: for<'a> IterType<'a, Item> {
// Backlink to `Family`.
- type Family: CollectionFamily;
+ type Family: MemberType<Item>;
-impl CollectionFamily for VecFamily {}
pub fn floatify<C: Collection<i32>>(ints: &C) -> <C::Family as MemberType<f32>>::Member
+where
+ C::Family: MemberType<f32>
{
And this works, but
- We've lost the supertrait bound, so we have to mention the
MemberType<_>
bounds everywhere - There's no actual guarantee a family implements
MemberType<T>
for allT
, where as you had to with the GAT - There's no actual guarantee you're working with a family as intended [1] since there can be a different implementation per input
T
, versus a single definition with the GAT
In summary, something GATs give us that we don't have today is for<T>
bounds in the form of
trait ForTSomeBound {
type SensibleName<T>: SomeBound;
}
They also make lifetime GATs nicer...ish. [2] I expect that to gradually improve over time as we get more implied and/or inferred bounds though.