Problem with mutable textures (mutability generally)

You are right. That solves the error but I don't understand why it solves it.

Yea, that's a whole can of worms. There's a bunch of little bits here regarding lifetimes, and I'm not sure which you're missing, or if the bits I have are correct :grin:. Here's my best stab at it.

Maybe you don't understand what 'a in this &'a Type notation means? I read this as, I've borrowed a value and that borrow will live for 'a time.

How long is 'a? Well, that's a million dollar question I'm still struggling with. Lifetimes are like playing connect the dots, and their time grows (or remains the same, but never shrinks) with the more dots you connect.

You created a Texture<'a> that borrows some texture data, and marked it's lifetime as 'a. Then you put that texture in RenderState<'b>, and connected 'a and 'b together (I think the relationship is 'a must live at least as long as 'b).

struct Texture<'a> {               // declare new lifetime 'a
    texture: sdl2::texture<'a>,    // connect 'a to the lifetime of texture data in sdl2 crate.
}

struct RenderState<'b> {                  // declare new lifetime 'b
    pub texture: RefCell<Texture<'b>>,    // connecting 'b to 'a
}

Then you called draw_batch, and created a mutable borrow of that RenderState with...

fn draw_batch(&'c mut self, render_state: &'c mut RenderState<'c>) {}

...and connected 'c directly to the lifetime of RenderState (I think 'c and 'b are now the same), and the dominoes fall and the borrow for self and render_state are now specified to live as long as 'b.

Changing the function signature like in the patch above is effectively the same as this code (I'm not sure if order matters in <'d, 'e>):

fn draw_batch<'d, 'e>                        // declare two lifetimes 'd and 'e
    ( &'d mut self                           // connect lifetime of borrow to 'd
    , render_state: &'d mut RenderState<'e>  // connect lifetime of borrow to 'd,
                                             // then connect lifetime of RenderState value to 'e
    ) {}

Now 'e is tied to the lifetime of RenderState ('e == 'b?), but 'd is given a default value when you invoke draw_batch at the time you invoke draw_batch, and that default value is the lifetime of the function call.

If this is confusing, welcome to my world!

If anyone sees any glaring errors, please point them out, and I'll strike-through and update this comment.

Yes it does!

Your explanation is super! And yes, it still confuses me. I mean, I've got the grasp at how lifetimes work but putting them in practice is a whole different story. There are so many things to consider that it sort of gets in the way while you're looking at the business problem rather than the implementation itself.

I think this is just a learning curve/experience issue. In fairly complex code, with lots of lifetimes flying around, I suspect most people, including Rust experts, will find it challenging to sort everything out in their heads. Instead, as mentioned, start with specifying as few lifetimes as possible (i.e. use lifetime elision), and then work "backwards" based on the compiler's error messages.

It may also help to keep the following in mind:

  1. Lifetimes are there to prevent dangling references. In its simplest form, a struct Foo<'a> { s: &'a str } ensures that a given Foo value does not outlive the string slice reference it's given (if it did, it would be left with a dangling reference).
  2. When mutability comes into the picture, lifetimes prevent dangling references in a similar manner to #1 above. A mutable reference cannot point to something that lives less than it.
  3. Rust has the notion of subtyping/variance, but in a different sense from some of the OO languages you may be familiar with. In Rust, subtyping/variance is all about lifetimes. For example, using the Foo from above, can a Foo<'static> be substituted for something expecting a Foo<'a>? It can if variance is allowed. So a longer lifetime is a subtype of a shorter lifetime (and this makes sense for immutable references, if you think about it). This is called lifetime narrowing/squeezing - a longer lifetime can be squeezed/shortened to the shorter lifetime. What if mutability is in play? Can a longer lifetime be substituted for a shorter lifetime? No, because then code expecting the shorter lifetime may set the (longer) reference to something that's not valid for the longer lifetime (i.e. &'static mut T cannot be squeezed down to some &'a T because then someone may store a &'a T into that &'static mut T, but that won't live as long - dangling reference). The nomicon talks about this in detail: Subtyping and Variance - The Rustonomicon
  4. Generic lifetime parameters (e.g. struct Foo<'a>, fn bar<'a>(x: &'a str), etc) are conceptually similar to generic type parameters (e.g. struct Foo<T>, fn bar<T>(...)). We don't know the exact type - caller decides (that's true in both cases). If we want to constrain the type/lifetime so the rest of our code works, we use constraints/bounds (e.g. T: Eq, <'a, 'b: 'a>, etc). Otherwise, we treat them abstractly.

Knowing the above doesn't necessarily mean you'll never get borrowck errors, but it helps in understanding what the borrow checker is complaining about. Keeping these things in mind also helps while writing the code since, over time with more experience, you'll get the gist of what invariants you need to maintain for the borrow checker to be pleased.

Not sure if this helps (or just confuses you more), but thought I'd jot it down.

1 Like