Variance of lifetime arguments in GATs

Macro?

If we're bikeshedding names, I'd suggest covar and contvar.

My primary concern here is for the convenience of the trait consumer. If I'm writing a complex library that provides a trait implementor, I'm okay with writing some boilerplate, but I absolutely do not want authors of downstream crates to have to fight with weird lifetime errors in situations that would normally Just Work (and that's definitely gonna happen, even if the documentation warns you about it). The upcast_gat approach is definitely useful as far as making it work at all - I actually thought of the upcast_gat approach as a workaround before I posted this thread - but it's not terribly satisfying as a solution. (And a macro wouldn't help with that aspect, because the problem for downstream crates is that they have to think about this issue in the first place.)

1 Like

A macro would mean the user just types

impl Foo for Bar {
  gat!('a, MyStruct<'a>);
}

Makes it harder to get it wrong.

The issue is that while a macro can help the implementor of the trait out, it does nothing to help the consumer of the trait, who has to know about the explicit variance cast.

7 Likes

I'm a bit late to this, but last year I attempted a number of a similar techniques for managing variance in sundial-gc), it proved to be untenable. At the time I did not see a way forward without #[covariant] or some other form of compiler support.

Prior conversations:

  1. Variance & GAT

  2. Experimenting with covariant associated types.

1 Like

For what it's worth, in the project that inspired this thread, I've started using the "unsafe trait and wrapper struct" approach, like so:

#[repr(transparent)]
pub struct TypedHandleRef<'a, E: EntityKind, H: EntityHandleKindDeref> {
  wrapped_gat: H::TypedHandleRef<'static, E>,
  _marker: PhantomData<&'a ()>,
}

impl<'a, E: EntityKind, H: EntityHandleKindDeref> TypedHandleRef<'a, E, H> {
  #[inline(always)]
  pub fn from_wrapped_gat(handle: H::TypedHandleRef<'a, E>) -> Self {
    Self {
      wrapped_gat: unsafe { mem::transmute_copy(&handle) },
      _marker: PhantomData,
    }
  }
  #[inline(always)]
  pub fn wrapped_gat(&self) -> &H::TypedHandleRef<'a, E> {
    unsafe { mem::transmute(self) }
  }
  #[inline(always)]
  pub fn into_wrapped_gat(self) -> H::TypedHandleRef<'a, E> {
    unsafe { mem::transmute_copy(&self.wrapped_gat) }
  }
}

And it seems to be working. This is fine for my use case, because I need the wrapper struct for other reasons as well (I want to be able to implement inherent methods and std traits for it, which you can't do for associated types).

Pity it requires transmute_copy instead of just transmute because Rust does not assume that A::B<'static> has the same size as A::B<'a>. But it's alright - this particular GAT is required to be Copy anyway, and it's fortunate that the ugliness is nicely encapsulated in a pretty simple wrapper.

1 Like

Perhaps Rust doesn't assume it, but the fact that lifetimes are a purely compile time construct would directly imply that A::B<'static> necessarily has to have the same size as A::B<'a>.

Specialization may break it in the future.

How so? I'm not aware of specialization based on lifetimes, only on types.

1 Like

Not with min_specialization, but I think the original RFC allows it.

No, lifetime specific is unsound. It's the whole reason why full blown specialization is unsound as currently implemented. And why min_specialization was developed

2 Likes

I know, but that's the point: we can't just block specializing on lifetimes, because it's too common and often hidden, even between crates (an example is specializing T on (T, T)).

That's exactly what min_specialization is doing though. It prevents lifetimes from being a factor.

This comes really close to working, but I can't figure out how to implement Life on any types.

This seems to work for my purposes, I still have to test it in the full library. see playground

trait Life {
    type L<'l>: 'l;
}

trait Co: for<'s> Life<L<'s> = Self> {}

impl<T: for<'s> Life<L<'s> = Self>> Co for T {}

...

#[test]
fn eq_test() {
     fn foo<'a, 'b, T: Eq + Co >(a: T::L<'a>, b: T::L<'b>, c: T) -> bool {
        #[allow(unused_assignments)]
         let mut v: &T = &a;
         v = &b;

         v == &c && a == b
     }

     foo::<usize>(1, 2, 3);
}

@elidupree Can the Co trait solve your issue?

I don't quite understand the purpose of your code - it looks like it's trying to solve a different problem than the one I had.

If you implement Life for a type, then you can use Co to require covariance.

Your original example, becomes fn eq<'a, 'b, T: Eq + Co>(a: T::L<'a>, b: T::L<'b>) -> bool, or you can even compare a T to a T::L<'_>.

trait Trait {
    type GAT<'a>: Eq;
}

fn eq<T: Trait>(left: T::GAT<'_>, right: T::GAT<'_>) -> bool {
    left == right
}

This only handles lifetimes, if you need to be covariant over types I don't think this can be adapted.

Unless I'm missing something, Co doesn't just require covariance, it requires the concrete type of the GAT to be the same regardless of the lifetime, rather than merely a subtype. In your example, it's forcing T::L<'a> to be usize itself rather than being any sort of temporary reference at all. How would my GAT, which actually does have a lifetime for a reason, be able to implement Co?

Your right, it does not work. Oh well.