Why is an explicit lifetime required here to resolve the associated type?

Hi, I'm working in a big codebase and ran into an issue recently that I would like to understand better.

I have reproduced a minimal example of the issue:

trait Ty {}
impl Ty for u32 {}

trait IntoFunc<A> {}
impl<F, A> IntoFunc<A> for F
where
    F: Fn(A),
    A: Ty,
{
}

fn func<A>(_: impl IntoFunc<A>) {}

trait T<'a> {
    type Type;
}
struct S {}

impl T<'_> for S {
    type Type = u32;
}

fn test(_: <S as T>::Type) {}

fn main() {
    func(test);
}

The problem is that <S as T>::Type is not equal to u32 in this example. From my understanding it should be the case because of the definition:

impl T<'_> for S {
    type Type = u32;
}

If I define the test function as fn test(_: <S as T<'static>>::Type) {} everything compiles fine, but why is the 'static required here?

Shouldn't <S as T>::Type just be a type alias for u32 in this case?

Additionally, if the structure S takes a lifetime it also needs to be explicitly written down as 'static for the code to compile e.g. <S<'static> as T<'static>>::Type.

I can't easily work around this because this part of the code <S as T>::Type is generated in a macro and I can't add 'static lifetimes to it, so I'm trying to understand why the compiler can't figure out that it just resolves to u32.

The underlying problem is that there are infinitely-many implementations of T for S: one for each possible lifetime. The compiler doesn't know which one it should use.

The trait definition allows T::Type to depend on the lifetime 'a. The compiler then rejects code that assumes it doesn't, to preserve forward compatibility. The fact that T::Type is the same for all implementations is considered a detail that could change in the future.

As for the fix, it's hard to say with your minimized example. There's no particular reason here for T to take a lifetime parameter, but I suspect your real use case does. If you can provide a bit more context around what the traits are actually doing, someone might be able to suggest a solution.

1 Like

Well, I think that changing the associated type on S would be a breaking change, so forward compatibility isn’t quite convincing. My best guess is that it’s a limitation of the type checker. Also one that shouldn’t be too hard to work around in practice.


Also @bkolobara, just in case that isn’t clear,

your code

fn test(_: <S as T>::Type) {}

is (AFAIK) short for

fn test(_: <S as T<'_>>::Type) {}

which further desugars to

fn test<'a>(_: <S as T<'a>>::Type) {}

So basically, test has an unnecessary lifetime parameter.

2 Likes

HRTBs can be used to make this compile:

fn test<U>(_: U) where for<'a> S: T<'a, Type=U> {}
2 Likes

Thanks @2e71828 and @steffahn, this makes it clearer. I have expanded the example into something looking more like the full use case:

trait Ty {}
impl Ty for u32 {}

trait IntoFunc<A> {}
impl<F, A> IntoFunc<A> for F
where
    F: Fn(A),
    A: Ty,
{
}

fn func<A>(_: impl IntoFunc<A>) {}

trait T<'a> {
    type Type;

    fn reference(r: &'a u32) -> Self;
}
struct SWithLifetime<'a> {
    r: &'a u32,
}

impl<'a> T<'a> for SWithLifetime<'a> {
    type Type = u32;

    fn reference(r: &'a u32) -> Self {
        Self { r }
    }
}

struct SWithoutLifetime {
    r: u32,
}

impl T<'_> for SWithoutLifetime {
    type Type = u32;

    fn reference(r: &u32) -> Self {
        Self { r: *r }
    }
}

fn test1(_: <SWithLifetime<'static> as T<'static>>::Type) {}
fn test2(_: <SWithoutLifetime as T>::Type) {}

fn main() {
    func(test1);
    func(test2);
}

Many structs implement the T, some of them define lifetimes (like SWithLifetime<'a>), some don't (like SWithoutLifetime ). The traits needs a lifetime because one of the functions (fn reference(r: &'a u32) -> Self;) requires a lifetime for it to be used with SWithLifetime<'a>.

The functions are defined as part of a macro:

fn test1(_: <SWithLifetime<'static> as T<'static>>::Type) {}
fn test2(_: <SWithoutLifetime as T<'static>>::Type) {}

The issue is that the macro can't automatically attach 'static to all structs, because some of them don't define a lifetime. I was looking for a unified way to say, use the T::Type no matter the lifetime, but as you explained this doesn't seem to be possible.

One solution I could think of is to pass SWithLifetime<'_,<AnotherSWithLifetime<'_>>> to the macro instead of SWithLifetime<AnotherSWithLifetime> and then recursively change the lifetimes to 'static as part of the macro transformation.

This makes it more verbose to use and more error prone, because if you don't pass the explicit lifetime the error message you get is going to be really confusing. I can't check in the macro if a struct actually has a lifetime to print out a better error message.

One thing you can do is split the trait into two parts: One that's dependent on the lifetime parameter and another that isn't. Since there's at most one implementation of the non-generic trait for each type, the conflict is resolved:

trait T2 {
    type Type;
}

trait T<'a>: T2 {
    fn reference(r: &'a u32) -> Self;
}

fn test1(_: <SWithLifetime<'static> as T2>::Type) {}
fn test2(_: <SWithoutLifetime as T2>::Type) {}
fn generic_test<U: T2>(_: U::Type) {}

(Playground)

3 Likes

Thanks! This brings me really close to a perfect solution. Ideally it would be:

fn test1(_: <SWithLifetime as T2>::Type) {}
fn test2(_: <SWithoutLifetime as T2>::Type) {}

so that both cases (with and w/o lifetime structs) can be generated in the same macro. But this causes an error:

error[E0277]: the trait bound `<SWithLifetime<'_> as T2>::Type: Ty` is not satisfied
  --> src/main.rs:53:5
   |
12 | fn func<A>(_: impl IntoFunc<A>) {}
   |                    ----------- required by this bound in `func`
...
53 |     func(test1);
   |     ^^^^ the trait `Ty` is not implemented for `<SWithLifetime<'_> as T2>::Type`
   |
   = note: required because of the requirements on the impl of `IntoFunc<<SWithLifetime<'_> as T2>::Type>` for `for<'r> fn(<SWithLifetime<'r> as T2>::Type) {test1}`
help: consider further restricting the associated type
   |
52 | fn x() where <SWithLifetime<'_> as T2>::Type: Ty {
   |        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Following the suggestion in the note doesn't resolve it.

Even defining T2 as:

trait T2 {
    type Type: Ty;
}

doesn't help.

I only skimmed the thread and just checked out your playground to play around, but following some suggestions quickly led to an ICE. Want to file the bug report? (Or I could, but I haven't attempted to actually grok the code yet.)

1 Like

I got a minimal example working and filed rust-lang/rust#80956

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.