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

associated types in generics cause lifetime invariance #115799

Open
tamird opened this issue Sep 12, 2023 · 3 comments
Open

associated types in generics cause lifetime invariance #115799

tamird opened this issue Sep 12, 2023 · 3 comments
Labels
A-variance Area: Variance (https://doc.rust-lang.org/nomicon/subtyping.html) C-bug Category: This is a bug. T-types Relevant to the types team, which will review and decide on the PR/issue.

Comments

@tamird
Copy link
Contributor

tamird commented Sep 12, 2023

playground

trait WithAssoc {
    type Assoc;
}

struct ImplsAssoc<'a> {
    phantom: core::marker::PhantomData<&'a ()>,
}

impl WithAssoc for ImplsAssoc<'_> {
    type Assoc = ();
}

struct Holder<W: WithAssoc> {
    foo: W,
    bar: W::Assoc,
}

struct CovariantHolder<W: WithAssoc<Assoc = Assoc>, Assoc = <W as WithAssoc>::Assoc> {
    foo: W,
    bar: Assoc,
}

// This doesn't compile.
fn invariant<'a: 'b, 'b>(x: Holder<ImplsAssoc<'a>>) -> Holder<ImplsAssoc<'b>> {
    x
}

// This compiles.
fn covariant<'a: 'b, 'b>(x: CovariantHolder<ImplsAssoc<'a>>) -> CovariantHolder<ImplsAssoc<'b>> {
    x
}
24 | fn invariant<'a: 'b, 'b>(x: Holder<ImplsAssoc<'a>>) -> Holder<ImplsAssoc<'b>> {
   |              --      -- lifetime `'b` defined here
   |              |
   |              lifetime `'a` defined here
25 |     x
   |     ^ function was supposed to return data with lifetime `'a` but it is returning data with lifetime `'b`
   |
   = help: consider adding the following bound: `'b: 'a`
   = note: requirement occurs because of the type `Holder<ImplsAssoc<'_>>`, which makes the generic argument `ImplsAssoc<'_>` invariant
   = note: the struct `Holder<W>` is invariant over the parameter `W`
   = help: see <https://doc.rust-lang.org/nomicon/subtyping.html> for more information about variance

It's not clear to me why Holder and CovariantHolder are any different. It's not possible construct a CovariantHolder<T, S> where S is anything other than <T as WithAssoc>::Assoc. Why does the compiler treat them differently?

@tamird tamird added the C-bug Category: This is a bug. label Sep 12, 2023
@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Sep 12, 2023
@saethlin saethlin added T-types Relevant to the types team, which will review and decide on the PR/issue. and removed needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. labels Sep 15, 2023
@WaffleLapkin
Copy link
Member

WaffleLapkin commented Sep 15, 2023

The answer is that "that's just how associated types work", when you have X::Assoc in a field, you are always invariant over X. t-types folks may have deeper insight here, but I think it would be correct to say Holder<T> <: Holder<U> <=> T <: U, T::Assoc <: U::Assoc, rust just chooses not to do that. The second example doesn't use associated types in fields, so it doesn't run into that problem and gets CovariantHolder<T, TA> <: CovariantHolder<U, UA> <=> T <: U, TA <: TU as normal, note that in this example there are no associated types in the subtyping, the bound is only needed to check validity of the types.

Again, I don't have the best insight here, but that's how I see it.

Another part to this (thanks to @danielhenrymantilla for suggesting), is that if you want Holder<T> <: Holder<U> <=> T <: U, T::Assoc <: U::Assoc and have consistent rules, then for this example:

struct S<I: Iterator>(I::Item);

You'll have to have S potentially be bivariant over I (since you'll have S<A> <: S<B> <=> A::Item <: B::Item which does not necessarily relate A and B). And bivarience seems to have always been a no-no in rust (again, t-types folks might know more about why).

@tamird
Copy link
Contributor Author

tamird commented Sep 15, 2023

You could imagine a rule that preserves covariance over type parameters if avoiding bivariance is a goal.

Hopefully t-types folks can chime in.

@tamird
Copy link
Contributor Author

tamird commented Sep 15, 2023

Batted this around with ChatGPT:

Rust's variance computation for structs operates mainly on type parameters. This is an intentional design decision rooted in simplicity and efficiency. Computing variance by deeply inspecting all fields would be a more complex process, both conceptually and computationally.

The situation with CovariantHolder does feel like a loophole. By decoupling the associated type from its parent type using generics (even if they are essentially representing the same type), you're directing the Rust compiler to treat them differently when it comes to variance.

It's worth noting that these kind of "tricks" or workarounds can sometimes have unintended consequences or become "gotchas" for developers reading the code, as they might not immediately understand why such a construct is in place.

Is it a bug? That's a matter of perspective. It's more of a side effect of the current design decisions made for variance computation in Rust. If this behavior causes issues in real-world code, it could be raised to the Rust community for consideration. They have a track record of refining and improving the language based on practical feedback.

real world example: #57440

@fmease fmease added the A-variance Area: Variance (https://doc.rust-lang.org/nomicon/subtyping.html) label Sep 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-variance Area: Variance (https://doc.rust-lang.org/nomicon/subtyping.html) C-bug Category: This is a bug. T-types Relevant to the types team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

5 participants