Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using associated type as struct field causes invariance w.r.t trait #57440

Closed
philipc opened this issue Jan 8, 2019 · 7 comments
Closed

Using associated type as struct field causes invariance w.r.t trait #57440

philipc opened this issue Jan 8, 2019 · 7 comments

Comments

@philipc
Copy link
Contributor

philipc commented Jan 8, 2019

The following code:

pub trait Trait {
    type Assoc: 'static;
}

pub struct I<'a>(pub &'a ());

impl<'a> Trait for I<'a> {
    type Assoc = ();
}

pub struct S1<T, A>
where
    T: Trait<Assoc = A>,
    A: 'static,
{
    pub t: T,
    pub a: A,
}

pub struct S2<T>
where
    T: Trait,
    T::Assoc: 'static,
{
    pub t: T,
    pub a: T::Assoc,
}

pub fn assert_covariance_i<'new, 'old: 'new>(i: I<'old>) -> I<'new> { i }
pub fn assert_covariance_s1<'new, 'old: 'new>(s1: S1<I<'old>, ()>) -> S1<I<'new>, ()> { s1 }
pub fn assert_covariance_s2<'new, 'old: 'new>(s2: S2<I<'old>>) -> S2<I<'new>> { s2 }

fails with:

error[E0308]: mismatched types
  --> src/lib.rs:31:81
   |
31 | pub fn assert_covariance_s2<'new, 'old: 'new>(s2: S2<I<'old>>) -> S2<I<'new>> { s2 }
   |                                                                                 ^^ lifetime mismatch
   |
   = note: expected type `S2<I<'new>>`
              found type `S2<I<'old>>`
note: the lifetime 'new as defined on the function body at 31:29...
  --> src/lib.rs:31:29
   |
31 | pub fn assert_covariance_s2<'new, 'old: 'new>(s2: S2<I<'old>>) -> S2<I<'new>> { s2 }
   |                             ^^^^
note: ...does not necessarily outlive the lifetime 'old as defined on the function body at 31:35
  --> src/lib.rs:31:35
   |
31 | pub fn assert_covariance_s2<'new, 'old: 'new>(s2: S2<I<'old>>) -> S2<I<'new>> { s2 }
   |                                   ^^^^

I would expect the variance of S1 and S2 to be the same w.r.t T. And even if this case can't be handled in general for all associated types, shouldn't the Assoc: 'static bound allow it?

I've been using S1 as a workaround for this issue, but the problem with that is the extra type parameter propagates upwards into all containing types, and it gets quite ugly and verbose.

@scalexm
Copy link
Member

scalexm commented Jan 8, 2019

You cannot expect to have variance for associated types in general. If T1 is a subtype of T2, there is no reason for T1::Item to be a subtype of T2::Item, as shown e.g. in this example: #21726 (comment)

Knowing that Trait::Assoc must outlive 'static, you can indeed deduce that the only lifetime that can appear in I<'a>::Assoc is 'static hence this family of associated types ought to be a constant family, but this example seems quite specific and I don’t know if we could draw a useful general rule out of this observation.

You can still write a hand-written « cast » between S2<I<'b>> and S2<I<'a>> when there is a relation between ’a and ’b. However this would incur an additional cost if you want to cast for example Vec<S<I<'a>>> to Vec<S<I<'b>>>: if you care about performances and you know for sure the relation holds (because you see « through » the associated types), you can still use transmute (well I think this is safe, right? cc @RalfJung)

@philipc
Copy link
Contributor Author

philipc commented Jan 8, 2019

You cannot expect to have variance for associated types in general. If T1 is a subtype of T2, there is no reason for T1::Item to be a subtype of T2::Item,

I understand that, and wasn't expecting that. Instead, I was hoping that internally rust could treat S2 as though it had an implicit A = T::Assoc type parameter, and given that, its variance could be handled exactly the same as S1.

You can still write a hand-written « cast » between S2<I<'b>> and S2<I<'a>> when there is a relation between ’a and ’b.

Do you mean like this?

pub fn cast_s2<'a, 'b: 'a>(s2: S2<I<'b>>) -> S2<I<'a>> {
    S2 { t: s2.t, a: s2.a, }
}

For that to be useful, I'd have to be able to write the cast to be generic in T: Trait, but then I don't see how to specify the lifetime relation.

@RalfJung
Copy link
Member

RalfJung commented Jan 8, 2019

Uh, I am not sure. Notice that you can have trait impls like impl Trait for I<'static>, and pick a specific associated type there.

In general, I don't see any reason why associated types should have any variance, ever. But I am also not an expert on this.

@philipc
Copy link
Contributor Author

philipc commented Jan 8, 2019

Notice that you can have trait impls like impl Trait for I<'static>, and pick a specific associated type there.

I don't want I<'static>. I'll edit the original post to remove uses of 'static for I.

In general, I don't see any reason why associated types should have any variance, ever. But I am also not an expert on this.

I don't want variance of the associated types. For my use case, they will never have a lifetime. But using the associated type as a field prevents variance w.r.t the trait type, which I do want.

@scalexm
Copy link
Member

scalexm commented Jan 8, 2019

@RalfJung

Uh, I am not sure. Notice that you can have trait impls like impl Trait for I<'static>, and pick a specific associated type there.

Yep, but if there is a generic impl impl<'a> Trait for I<'a>, and the associated type must outlive 'static, the associated type value must be the same among all I<'a> types.

But actually what I was asking is, if you know that I<'a> is indeed a subtype of I<'b> (but the compiler cannot enforce it because there are associated types playing around), whether it was safe to transmute Vec<I<'a>> to Vec<I<'b>>.

@philipc

Do you mean like this?

pub fn cast_s2<'a, 'b: 'a>(s2: S2<I<'b>>) -> S2<I<'a>> {
    S2 { t: s2.t, a: s2.a, }
}

For that to be useful, I'd have to be able to write the cast to be generic in T: Trait, but then I don't see how to specify the lifetime relation.

Yes that's what I was meaning. Annoying, but the only solution I guess. Basically you have to manually write casts for all your types that need sub-typing. If there are only a few, that might work.

@RalfJung
Copy link
Member

RalfJung commented Jan 8, 2019

But actually what I was asking is, if you know that I<'a> is indeed a subtype of I<'b> (but the compiler cannot enforce it because there are associated types playing around), whether it was safe to transmute Vec<I<'a>> to Vec<I<'b>>.

Well, that's pretty much the definition of subtype -- "it is safe to do the transmute". So, yes -- but now the hard part is making sure that it's really a subtype.

@philipc philipc changed the title Using associated type as struct field causes invariance Using associated type as struct field causes invariance w.r.t trait Jan 17, 2019
@philipc
Copy link
Contributor Author

philipc commented Feb 22, 2019

Changing the workaround to the following avoids most of the downsides, since it lets me omit the extra type parameter in uses of the struct. It's still something I would expect the compiler to allow automatically, but I think this is good enough for me for now.

pub struct S1<T, A = <T as Trait>::Assoc>
where
    T: Trait<Assoc = A>,
{
    pub t: T,
    pub a: A,
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants