How did you become proficient with lifetimes?

How did you become proficient with lifetimes beyond tutorial examples, i.e. real life coding? Was it familiarity with C++ templates, C raw memory, long history of reading Rust mail lists, other?
Were there any mental tricks which helped you to understand it better and interpret lifetime error messages?

1 Like

Mainly many hours of real life coding and running into lifetime problems that I never wished for.

Reading the compiler error messages and trying to decipher which meaning I could think of is what it's trying to say, and then realizing the way the compiler message says it is actually really clear, once you understand it.

Also, the beginners' IRC chat and this forum have been places where I've had huge help from other Rust coders (thanks :bowing_man:).

Things that I didn't learn from the book / googling, but from reading this forum and coding are:

  • Lifetime parameters are used to increase the lifetime of a reference, never to decrease it (paraphrased from somewhere, I can't find it in this forum).

  • Distinction between input and output lifetimes:

    'static as a lifetime parameter for a returned object (e.g. function's return value) doesn't mean the object you are returning is created as a static variable, but rather the lifetime referenced by the object's fields can be statically determined.

    The lifetime parameters on functions that are used for both the argument and the return value are then saying, "this return value's lifetime is determined by the argument's lifetime"

  • You can't have a "dangling ghost lifetime variable":

    If you have:

    • struct Builder<'f> { member: MyBoxedFunction<'f> }, where
    • MyBoxedFunction<'f> is a type that boxes a function

    you can't create a consuming build(self) function that invokes the member function with an argument created within that build function. Even though MyBoxedFunction only needs a reference to the argument for the duration of the function call, Rust says "actually the argument needs to live as long as 'f", which must be greater than the Builder's lifetime.

    That's when I discovered higher ranked trait bounds, and discovered that you don't need to declare the <'f> for the Fn traits.

5 Likes

I strongly suspect a mini-book can be written on just the lifetimes portion of Rust, and all the different ways they manifest. There’s also something to be said about the difference in knowing how lifetimes work and deciphering compiler error messages; a lot of the latter, particularly beyond the “obvious” cases, is just experience - at some point, you know exactly (or close to it) what the compiler is saying. Not because you necessarily find its message crystal clear, but because you’ve seen it before and spent time figuring it out. Then it’s the same pattern(s) repeating in different disguises.

The mental framework I use is to always look at it from the standpoint of “what type of memory unsafety is the compiler trying to prevent here? What could the code I just wrote, that doesn’t compile, do that would be unsafe?”. Then there’s the skill/experience of knowing what you want to express to the compiler but learning the syntax and formulation of how to say it.

Once you’re somewhat out of the water, then come mutable references and they change variance/subtyping - so now you need to learn about what subtyping means and how different borrows and types interplay in that light.

Then you have generic code, with its own lifetime formulations.

Then you have papercuts, such as the ones that NLL fixes. So you learn about those and try to avoid them or work around them.

Then you learn about how lifetimes don’t project with associated types sometimes. Or there’s no variance of lifetimes in types that appear as generic type parameters on traits. Or how last expression of a block extends the borrow to outside the block.

Then you also learn about trait objects and default object bounds.

Then you may come across situations, such as what you had I believe, where you want generic associated types but that feature is missing at the moment.

Then there’s HRTB. And then you want to somehow assemble all of these things in a delicate way and write non-trivial projects. And if you’re creating APIs/abstractions, you almost need to know a lot of this upfront so you can predict whether you’ll design yourself into some corner.

And there’s probably other stuff that I’m missing. The bottom line is it’s completely non trivial and you should expect for it to take a while to sink in. Not so much the high level handwavy parts of what they are, but for how to actually get non trivial work done and make use of them. The best way is to simply log the required mileage with real code and some head banging against the errors. Some will be real, some will be papercuts, and others missing features.

So, continue to use this forum (and other channels) for help without mercy :slight_smile:.

12 Likes

This is my intention, at some point...

5 Likes

For me it took:

  • Unlearning C. I kept causing problems for myself by trying to use references as if they were pointers (spoiler: they're not). In C most problems are solved with pointers, and value types and pass-by-value is avoided. OTOH in Rust owned values — which look like passed-by-value — are common and often required.

  • Accepting that Rust does things the "Rust way", not "my way", e.g. it can't reason about a struct that contains references to itself. Alternative approaches, sometimes a completely different architecture, may be required. Writing things in a way that is easy for the borrow checker makes Rust easy.

  • Getting to know Rust quirks and patterns, e.g. when you use &Option<T> type, you have to know about .as_ref(). I've tried to document them here.

4 Likes