Lifetimes driving me batty!

Hello, this is my first ever question here.

I've tried for too long to get this (stripped-down summary) to compile.

I want to build a chain of responsibility composed of hooks chained together. Feed a type instance, a String in this case, and out the end comes a modified type instance (a String in this example).

Here is a permalink to the playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e45b9c94efda337d5870472e9ebdf00b

I'm getting This parameter and the return type are declared with different lifetimes... from the compiler.

I swear, I've tried a hundred variants. This seems so simple, though...

Thanks for your help!

Steve

use std::option::Option;

#[derive(Debug, Clone)]
pub struct Hook<'a> {
    pub hook: Option<Box<&'a Hook<'a>>>,
}

pub trait Hookable {
    type Thing;
    fn process(&self, t: &mut Self::Thing) -> &mut Self::Thing;
}

impl Hookable for Hook<'_> {
    type Thing = String;        
    fn process(&self, t: &mut Self::Thing) -> &mut Self::Thing {
        &mut t
    }                   
}

fn main() {
    let mut h1 = Hook {
        hook: None,
    };
    let h2 = Hook {
        hook: None,
    };
    h1.hook = Some(Box::new(&h2));
    let mut x = "xyz".to_string();
    h1.process(&mut x);
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0623]: lifetime mismatch
  --> src/main.rs:16:9
   |
15 |     fn process(&self, t: &mut Self::Thing) -> &mut Self::Thing {
   |                          ----------------     ----------------
   |                          |
   |                          this parameter and the return type are declared with different lifetimes...
16 |         &mut t
   |         ^^^^^^ ...but data from `t` is returned here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0623`.
error: could not compile `playground`.

To learn more, run the command again with --verbose.

1 Like

According to the lifetime elision rules, the output gets its lifetime from &self, not t. The error message doesn't tell you that elision is happening or that the output lifetime is coming from &self, but that's what's happening.

Add an explicit lifetime parameter (in both the trait and the impl) and use it for t and the output, and that error will go away.

(Also t is already the reference you want to process and return, you can't (and shouldn't) return a reference to that reference, just return t)

This works (edited)

6 Likes

FYI, I think you pointed at the wrong playground @kennethuil

you're right, I updated the link.

Thank you Kenneth @kennethuil! This is very helpful, and it helps me to better understand.

Lifetimes are hard. I'll be happy to transition to the Next Hard Thing :slight_smile:

It's worth noting that these rules are listed in the Rust Reference - it's a good idea to familiarize yourself with these!

When I run into lifetime issues like this, I find it a helpful exercise to explicitly write out the lifetimes on the function - if this changes/fixes the error, that means the elided lifetimes were doing something different to what you expected.

6 Likes

This is a really good tip @17cupsofcoffee! :facepunch:

Documentation on lifetimes is somewhat scattered. I looked everywhere, and tried so many combinations. I have never seen the Lifetime elision topic you've linked. Thanks for that. @17cupsofcoffee.

I think what the Rust docs need is all the ways lifetimes can be applied, in one view. Like, impl like this, struct like this, functions like this, parameters like this, return values like this, with the mutable and borrowed variants included, where applicable. That would ve very valuable.

2 Likes

@17cupsofcoffee I found myself wishing the following, bearing in mind that I had (still have) incomplete grok of how lifetimes are notated in code.

I was thinking, I'm writing a small utility that's never resident, never large in memory, and called once and done. It may get called 100,000 times, but its physical instance lifetime is milliseconds, or less.

What I really wanted, out of sheer frustration, is a directive or macro that says, in effect, scope everything to the maximum lifetime. Scope everything to 'static lifetime, essentially.

I sense this is sematically wrong but, at the same time, why is THAT stopping all progress, right now?

Lifetimes, at my Rust experience level, feel like a barrier to entry.

Your experience here is the general case. "Fighting the borrow checker", which includes dealing with lifetimes, is almost universally the hardest part of learning to write Rust code.

3 Likes

Bear in mind that the reason this doesn't exist is probably because it's unclear what you want. Objects live from when they're created until they fall out of scope, and lifetime annotations on structs/functions act as declarations of data dependencies. The only way to make everything maximally scoped is to create all of your data in the broadest scope (all data is defined at the top of main, or in static memory) or you stop using references and start passing around owned data via Box, Rc, and friends.

Lifetime annotations do not have any impact on your memory usage. The compiler is not using lifetime annotations to decide where to invalidate (or deallocate) objects, it's using them only to ensure you never actually access an invalid object.

The rules take some time to learn, and you'll need a fair bit of practice to learn how to structure your applications around them, but once they are internalized, you should find that it is no longer a barrier for you except in the most contrived or complicated situations.

4 Likes

Ok! Thanks for that @skysch, that's very helpful to my understanding.

May I ask you a followup?

In my fn main() {...} I am building a potentially elaborate directed graph of chained hooks. Each hook is responsible for an atomic transformation to the workpiece. Once the directed graph of hooks is constructed, feed the workpiece to the directed graph, and what returns is the mutated workpiece. It's a pipeline, basically.

In my current state of grok, everything should be scoped to fn main() {...} because the hook chains, and the workpiece, live and ultimately die by the scope of fn main() {...}.

So here's what puzzles me: In my code (playground) (which works fine) there is no lifetime notation in fn main() {...}.

What am I missing to understand why fn main() {...} doesn't define or proscribe lifetime, in any obvious way?

The Rust compiler generally only needs you to tell it about lifetimes that appear in the signature of functions and in types. The variables inside main don't appear in the signature of main, so the compiler will figure them out without your help.

It's also worth saying that "dropped at the end of main" is shorter than 'static.

Another way to say it is: The Rust compiler does not look inside other functions when it does borrow checking, and the way it knows what other functions do is it looks at their signature. By looking at how variables are used and on the signatures on functions that are called, it can detect any possible violations such as use-after-free.

3 Likes

Bingo. Thank you @alice that clarifies it!

Thank you, everyone, for helping me better understand Lifetimes.

I'd like to buy 'yall lunch.

Steve

Lifetimes can be hard, therefore I wrote a little article about them, aim to put tricky things simple. Maybe that is of help, so here the shameless plug: my post on lifetimes made easy

2 Likes

Thank you @sassman that article is very helpful!

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.