tfpk / lifetimekata

An exploration of lifetimes in Rust.
https://tfpk.github.io/lifetimekata
Apache License 2.0
260 stars 55 forks source link

Claim of lifetime sizes in 00_welcome #18

Open mordrax opened 1 year ago

mordrax commented 1 year ago

Towards the end of the first chapter, once the 'variable and 'reference lifetimes has been established, there is the following statement:

We call a region of code where a variable exists a "lifetime". We can give lifetimes names using the syntax 'name. So if we call the variable's lifetime 'variable, and the reference's lifetime 'reference, we can then formally say that for any variable that references another variable, 'variable must be larger than 'reference.

The last part of this statement is confusing to me because it does not seem to be true for all cases.

Let's say in the code example, that the inner scope of 'variable is expanded to include the if let statement:

fn main() {
    let mut my_reference: Option<&i32> = None;

    // Starting a scope.
    {
        // my_variable created                               // \ \
        let my_variable: i32 = 7;                            // | |
        my_reference = Some(&my_variable);                   // | |- my_variable exists here. ('variable)
        // At the end of the scope, `my_variable` is dropped // | |
        drop(my_variable);                                   // | |
        // my variable destroyed                             // | /
    // inner scope does not stop here }
                                                             // |
    if let Some(reference) = my_reference {                  // |
        println!("{}", reference);                           // |
    }                                                        // /
    } // inner scope now stops here
}

It is not true that 'variable is now larger than 'reference, because 'reference is declared earlier still.

So this is confusing to me. It would make more sense if it read:

we can then formally say that for any variable that references another variable, 'variable must live longer than 'reference.

tfpk commented 1 year ago

Hey! So there are actually a couple things going on here, apologies if this is unstructured; and also apologies if I over-explain things (if nothing else, hopefully I can point other people here with similar issues).

First: the example you gave actually isn't quite doing what you think it is. This is because i32 is a type which is Copy. Therefore, when you call drop(my_variable), you're actually copying my_variable, and basically calling drop(7).

A code example which gets around this issue is:

#[derive(Debug)]
struct NotCopy {
    i: i32
}

fn main() {
    let mut my_reference: Option<&NotCopy> = None;

    // Starting a scope.
    {
        // my_variable created                               // \ \
        let my_variable: NotCopy = NotCopy { i : 7};         // | |
        my_reference = Some(&my_variable);                   // | |- my_variable exists here. ('variable)
        // At the end of the scope, `my_variable` is dropped // | |
        drop(my_variable);                                   // | |
        // my variable destroyed                             // | /
        // inner scope does not stop here }
                                                             // |
        if let Some(reference) = my_reference {              // |
            println!("{reference:?}");                       // |
        }                                                    // /
    } // inner scope now stops here
}

But what you'll see in this example is that we do run into an issue:

error[E0505]: cannot move out of `my_variable` because it is borrowed
  --> src/main.rs:15:14
   |
13 |         my_reference = Some(&my_variable);                   // | |- my_variable exists here. ('variable)
   |                             ------------ borrow of `my_variable` occurs here
14 |         // At the end of the scope, `my_variable` is dropped // | |
15 |         drop(my_variable);                                   // | |
   |              ^^^^^^^^^^^ move out of `my_variable` occurs here
...
19 |     if let Some(reference) = my_reference {                  // |
   |                              ------------ borrow later used here

The error here is with the drop, because that's the first point where rust can't figure out what to do: before that point, it's happy to give you a borrow of my_variable, but at that point it's either got to move my_variable, or let you keep that borrow (and since it can't do both, it's an error!).

I do think that the wording here could be improved, and I'm gonna have a think about how to do that (suggestions welcome) but before I do, I'd like to know if this explanation helped, or if there's something you're still confused about. That'll help me figure out what the best wording actually is!

Thanks for opening an issue!

mordrax commented 1 year ago

Hey @tfpk , thanks for responding promptly!

I think I follow what you're getting at with drop but it's raising more questions than it answers.

So reading 00_example, I chucked this into the playground and it works. ie, putting println! inside the inner scope, but after the drop prints 7 (https://play.rust-lang.org/?version=beta&mode=debug&edition=2021&gist=62de6293aff89f91d15e7d57b9446bce).

So now I'm confused about several things.

  1. Why does my_reference work, when it's referencing my_variable and my_variable is dropped on ln 15 and my_reference is used on ln 19. Is that because once dropped, the Copy trait implicitly does a copy of my_variable and keeps that variable around, what is it's lifetime? Or is it because my_reference just refers to &7 which permanently exists and has a 'static lifetime?
  2. How does drop(my_variable) differ from closing the scope { } because in the playround, if I was to put the println! outside the scope, then it errors as expected, the compiler calls out that my_variable is no longer in scope at the time my_reference is being used in the println!, however, if the println! is inside the scope { } but after dropping my_variable, it still works. so there's some differences there.
  3. My OP question still remains, in both cases, the scope of my_variable is smaller than my_reference, that is because my_reference's scope is from the start of main() to the end, so perhaps this is rust jargon that I don't get yet, but my understanding is that the lifetime of 'variable has to end later than the lifetime of the things that reference it, ie `'my_reference'

I think there's two things here, a simpler explaination of lifetime and an accidental complexity introduced by this example. And we're now talking about both!

tfpk commented 1 year ago

Sorry to pile on the confusion, let's try get it straightened out...

  1. The reason for this is because calling drop(my_variable) (where my_variable is Copy) is identical to drop(my_variable.clone()). That's what it means for a variable to be copy, and hopefully here you can see that it would mean that the drop function basically has no effect.
  2. If a variable exists within a scope, it cannot be accessed from outside that scope. Trying to use its name is an error because outside that scope, the name simply does not exist. Calling drop does not cause the name to stop working, it simply moves the value. Drop is literally the function pub fn drop<T>(_x: T) { } (i.e. it moves the value, then does nothing with it). This is the same as any moved value.

To help with 1 and 2, the reading the documentation of the drop function might help!

  1. Yeah I think I somewhat dodged this question, apologies!. The important thing here is this: the lifetime of a reference is always smaller than the lifetime of the variable it refers to. Here's an example of what I think you're getting at:
#[derive(Debug)]
struct NotCopy {
    i: i32
}

fn main() {
    let mut my_reference: Option<&NotCopy> = None; // lifetime of my_reference starts

    let my_value: NotCopy = NotCopy { i: 8 }; // lifetime of my_value starts

    my_reference = Some(&my_value);

    if let Some(val) = my_reference {
        println!("{val:?}");
    }

}

Which seems to disprove my comment made in section 0. The trick here is that there are two seperate lifetimes which are being conflated here: the lifetime of my_reference (the variable), and the lifetime of &my_value (which is being stored inside my_reference. The documentation kind of confuses the issue here (but I'm gonna have to think about how to fix it, because it's hard to say non-confusingly).

It is clear that the variables my_reference and my_value do not necessarily have to share a lifetime on their own. The trick is that &my_value changes things: clearly, the reference &my_value must have a smaller lifetime than my_value (you can't get a reference to something before it exists, and Rust won't let you keep a reference around after the value being referenced is gone).

Okay I've had this checked; pretty sure it's all reasonable.

tfpk commented 1 year ago

I've tried to improve the wording in #19, but it's a first draft which I'll have to revise again, I suspect.