Confused about traits, generics and/versus associated types

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 :slightly_smiling_face:.

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.

We can push forward though...

-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 all T, 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.


  1. floatify there goes from a HashSet to a Vec ↩ī¸Ž

  2. It's considered a big enough deal that we still don't have lending iterators, etc, in std. ↩ī¸Ž

1 Like