Lifetime nuances between `type` and `Self`

Confusion about Self 's lifetime

use std::str::CharIndices;

pub enum TokenKind<'a> {
    Ident(&'a str),
}

pub struct Token<'a> {
    kind: TokenKind<'a>,
}

impl Token<'_> {
    // cannot be compiled: lifetime may not live long enough
    //                   <'1>          <'2>     <'_>
    fn parse_ident(i: CharIndices) -> (Self, CharIndices) {
        (
            Token {
                kind: TokenKind::Ident(i.clone().as_str()),
            },
            i,
        )
    }
}

Compiler says that parse_ident is supposed to return data with lifetime '2, but it is returning data with lifetime '1 .
But when I change Self to Token, it can be compiled.

Why is there an error when returning Self and an Ok when returning Token, are they different when we impl Token<'_> and impl Token?

I also found that Self can also be compiled when the Token is not pub. Does pub have any effect?

You have a lot of elided lifetimes in your code — this can make things confusing. It's good to give some of them names to clarify. Let's start with this one:

-impl Token<'_> {
+impl<'a> Token<'a> {

This doesn't change the meaning; it just gives a name to something that had been anonymous (the compiler called it '2). Now, when you write Self, that means exactly the type Token<'a>, no matter what. But when you write Token in the return type, that's Token with another elided lifetime, and according to the lifetime elision rules, Rust takes the lifetime as being the one from CharIndices. Now, that association is fine — but it's not the same as the lifetime in the impl, which it should be. The fix is to specify that the lifetime for CharIndices is the same as the lifetime in the Self type the impl block introduces:

impl<'a> Token<'a> {
    //        ^^^^ this...
    fn parse_ident(i: CharIndices<'a>) -> (Self, CharIndices<'a>) {
        //                       ^^^^ ...must equal this

I recommend that, at least while you are learning Rust, you should activate this lint for your program:

#![deny(elided_lifetimes_in_paths)]

This will make the compiler insist that you specify at least <'_>, if not an actual named lifetime, on each named type that has a lifetime parameter; this way, you will always remember that there is a lifetime involved, and have an opportunity to think about what lifetime it should be.

3 Likes

When we have some complex lifetimes problem, it's usually a good idea to make every lifetime explicit; the compiler can help you with elided_lifetimes_in_paths lint. Let's apply it to your current code:

error: hidden lifetime parameters in types are deprecated
  --> src/lib.rs:16:23
   |
16 |     fn parse_ident(i: CharIndices) -> (Self, CharIndices) {
   |                       ^^^^^^^^^^^ expected lifetime parameter
   |
note: the lint level is defined here
  --> src/lib.rs:1:9
   |
1  | #![deny(elided_lifetimes_in_paths)]
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^
help: indicate the anonymous lifetime
   |
16 |     fn parse_ident(i: CharIndices<'_>) -> (Self, CharIndices) {
   |                                  ++++

error: hidden lifetime parameters in types are deprecated
  --> src/lib.rs:16:46
   |
16 |     fn parse_ident(i: CharIndices) -> (Self, CharIndices) {
   |                                              ^^^^^^^^^^^ expected lifetime parameter
   |
help: indicate the anonymous lifetime
   |
16 |     fn parse_ident(i: CharIndices) -> (Self, CharIndices<'_>) {
   |                                                         ++++

Then, now that every lifetime is here (even if it's inferred), let's apply the lifetime elision rules and give each of them a name (updated playground):

impl<'token> Token<'token> {
    fn parse_ident<'char_indices>(i: CharIndices<'char_indices>) -> (Self, CharIndices<'char_indices>) {
        // snip
    }
}

And finally, replace Self with the exact type it stands for:

impl<'token> Token<'token> {
    fn parse_ident<'char_indices>(i: CharIndices<'char_indices>) -> (Token<'token>, CharIndices<'char_indices>) {
        // snip
    }
}

Now the compiler can show you the problem more cleanly:

error: lifetime may not live long enough
  --> src/lib.rs:15:9
   |
13 |   impl<'token> Token<'token> {
   |        ------ lifetime `'token` defined here
14 |       fn parse_ident<'char_indices>(i: CharIndices<'char_indices>) -> (Token<'token>, CharIndices<'char_indices>) {
   |                      ------------- lifetime `'char_indices` defined here
15 | /         (
16 | |             Token {
17 | |                 kind: TokenKind::Ident(i.clone().as_str()),
18 | |             },
19 | |             i,
20 | |         )
   | |_________^ associated function was supposed to return data with lifetime `'token` but it is returning data with lifetime `'char_indices`
   |
   = help: consider adding the following bound: `'char_indices: 'token`

That is, lifetimes of the token and of the CharIndices iterator are independent in the signature, but the implementation requires them to be dependent - since CharIndices borrows from its input, and Token consumes this borrow, its lifetime must not be longer then the input's scope.

On the other hand, if you replace Self with Token and do the same thing, you arrive to another signature:

impl<'token> Token<'token> {
    fn parse_ident<'char_indices>(i: CharIndices<'char_indices>) -> (Token<'char_indices>, CharIndices<'char_indices>) {
        // snip
    }
}

Which is exactly what I described above: the borrow in Token is the same as the borrow in CharIndices; but it's probably not what you want (@kpreid beat me to that, so I just refer to what is already said).

Could you share the reproduction case? When I simply drop the pub in your code on playground, the error is exactly the same as before.

3 Likes

Got it. Thanks a Ton.

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.