Why can't I refine RPIT if method takes GAT?

This compiles if Bar is not generic over any lifetime. Shouldn't it also compile if it is?

trait Foo {
    type Bar<'a>;

    fn foo(&self, bar: Self::Bar<'_>) -> impl Debug;
}

struct MyFoo;

impl Foo for MyFoo {
    type Bar<'a> = ();

    fn foo(&self, _: Self::Bar<'_>) {}
}

Playground.

2 Likes

You are not returning anything in the implementation. I think the error comes from rustc not being able to infer the lifetime of the returned object.

If you include the impl Trait by this:

impl Foo for MyFoo {
    type Bar<'a> = ();

    fn foo(&self, _: Self::Bar<'_>) -> impl Debug {}
}

It compiles without problem. Probably someone can explain it in more detail, but I suspect the compiler is actually including the self lifetime into the impl return, like:

fn foo<'s>(&'s self, bar: Self::Bar<'_>) -> impl Debug + use<'s>;

So your implementation does not have the same lifetime as the declaration.

You're supposed to be able to refine the method to provide more than the trait demands in a way that downstream can take advantage of.


I would search for an issue about this and file a new issue if you don't find one. (Please report back if you do so.)

Edit: This is a flavor of this issue. I created a diagnostic issue too.

This works and will generally normalize away (but I agree the OP should work too).

I'm trying to figure out what is happening. This is specifically about return-position-impl-trait-in-traits (RPITIT) which is more specific than just return-position-impl-traits (RPIT) as mentioned by lcnr.

I'm sure you have already found out, but you can get it to work by only using one lifetime parameter which in turn requires a bound like Self: 'a for example:

trait Foo {
    type Bar<'a>
    where
        Self: 'a;
    fn foo<'a>(&'a self, bar: Self::Bar<'a>) -> impl Debug;
}
struct MyFoo;
impl Foo for MyFoo {
    type Bar<'a>
        = ()
    where
        Self: 'a;
    fn foo<'a>(&'a self, _: Self::Bar<'a>) {}
}

Of course that changes the definition of Foo though.

This part of the RFC is the closest I could find that goes over how "awkward" desguaring becomes when multiple lifetimes are involved which is the case for your example since without elision the trait def is:

trait Foo {
    type Bar<'a>;
    fn foo<'a, 'b>(&'a self, bar: Self::Bar<'b>) -> impl Debug;
}

Good find.

Nice. I like that workaround better since mine requires changing the definition of Foo.

Interesting. It's not even correct to say that multiple lifetimes are the problem since the below also doesn't work:

trait Foo {
    type Bar<'a>;
    fn foo(bar: Self::Bar<'_>) -> impl Debug;
}
struct MyFoo;
impl Foo for MyFoo {
    type Bar<'a> = ();
    fn foo(_: Self::Bar<'_>) {}
}

Nit: without elision it's

fn foo<'a, 'b>(&'a self, bar: Self::Bar<'b>) -> impl Debug + use<'a, 'b, Self>;

...but that syntax is behind an unstable feature still.


I played around some more and I believe I've figured it out. It still deserves a diagnostic issue at a minimum.

When a method lifetime parameter participates in an explicit bound, it becomes "early bound" -- the method item type is parameterized by said lifetime, and you can turbo-fish it. But when a lifetime does not participate in any explicit bound,[1] it is "late bound" -- the lifetime appears in the notional implementation of the Fn traits, but not in the type of the method item itself, and it is not turbofishable. This is what we're used to happening with elided lifetimes.

The compiler lets you change the number of late bound lifetimes you put on a method in an implementation, but not the number of early bound lifetimes -- because in a generic context, you can still turbofish the early bound lifetimes, for example.

And here's what's happening: the -> use<..> are making the lifetimes early bound (even when it's elided). But in the refined signature, they were late bound. So here's another fix for the OP:

impl Foo for MyFoo {
    type Bar<'a> = ();
    //     vvvvvv makes `'a` an early-bound lifetime
    fn foo<'a:'a>(&self, _: Self::Bar<'a>) {}
    // This also works
    fn foo<'a:'a>(&self, _: ()) {}
}

I didn't realize this before, but this is apparently also how things work when there's a GAT in the output type.[2] Well.... but not always. I don't know what the "rules" actually are (or how intentional). Changing the use case of my workaround would be a breaking change though...


  1. implicit ones are ok ↩︎

  2. which is why one of my workarounds works ↩︎

2 Likes

As far as I understand, capturing all the lifetimes is the expected behavior until the use<...> syntax gets introduced (the compiler even warns you about it in stable), similar to how currently every GAT has the Self: 'a implicit bound.

Correct for -> impl Trait in traits, and also outside of traits in edition 2024. We have precise capturing (use<>) outside of traits already, but not in traits yet.

I'm not sure why you thought I needed clarification on that in the context of my post though.

No, GATs have the bounds that you declare on them. The compiler will force you to declare the bounds in certain situations, most commonly Self: 'a, but they're not implicit so far. See this issue and also here.

1 Like

I just wanted to be sure I understood it correctly. I've been experimenting with using more advanced generics in Rust, and I find hard to understand the "proper" rules, since the documentation is sparse at the moment.

1 Like

Interesting. I'm not familiar with use. Thanks for the correction.

Your grasp of Rust is enviable. This was a great reply. I still have so much to learn.

This is a flavor of this issue. I created a diagnostic issue too.

The early/late is intentional, because generic associated types don't constrain their lifetime parameters (since you don't have to use the parameter in the definition).[1]

As that's a soundness consideration, probably the only "fix" to the "bugs" is if trait implementations automatically inherit early-boundedness from the trait definition, instead of being implied by the signature... which is also kinda bleh.

Or some much wider change to early-vs-late bounds more generally, but that seems distant-future to me.


  1. breadcrumb ↩︎

3 Likes