Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do lifetimes in Rust impact mutability?

I am testing my understanding of lifetimes in Rust by explicitly annotating function signatures and I created an example that I am not sure I understand.

In this example, I am simulating the concept of sharing a book and turning a page within it. To do this I am using a single mutable reference which I pass to a borrow_and_read function that updates the curr_page field of a Book struct. My Book struct and main function look like:

#[derive(Debug)]
pub struct Book<'a> {
    pub title: &'a str,
    pub curr_page: Option<i32>,
    pub page_count: i32,
}

fn borrow_and_read<'a>(a_book: &'a mut Book<'a>) {
    match a_book.curr_page {
        Some(page) => a_book.curr_page = Some(page + 1),
        None => a_book.curr_page = Some(0),
    };
}

fn main() {
    let mut the_book: Book = Book {
        title: "The Book",
        curr_page: None,
        page_count: 104,
    };

    let a_book: &mut Book = &mut the_book;

    borrow_and_read(a_book);
    borrow_and_read(a_book);

    observe_book(&*a_book);
}

pub fn observe_book<'a>(a_book: &'a Book<'a>) {
    println!("Observing: {:?}", a_book);
}

(Playground)

For my first implementation of the borrow_and_read function, I let the compiler add annotations and everything compiled:

fn borrow_and_read(a_book: &mut Book) {
    match a_book.curr_page {
        Some(page) => a_book.curr_page = Some(page + 1),
        None => a_book.curr_page = Some(0),
    };
}

I then tried adding a single lifetime annotation specifying a lifetime for both the reference and the instance of the Book itself:

fn borrow_and_read<'a>(a_book: &'a mut Book<'a>) {
    match a_book.curr_page {
        Some(page) => a_book.curr_page = Some(page + 1),
        None => a_book.curr_page = Some(0),
    };
}

This yielded the following errors:

error[E0499]: cannot borrow `*a_book` as mutable more than once at a time
  --> src/main.rs:25:21
   |
24 |     borrow_and_read(a_book);
   |                     ------ first mutable borrow occurs here
25 |     borrow_and_read(a_book);
   |                     ^^^^^^
   |                     |
   |                     second mutable borrow occurs here
   |                     first borrow later used here

error[E0502]: cannot borrow `*a_book` as immutable because it is also borrowed as mutable
  --> src/main.rs:27:18
   |
24 |     borrow_and_read(a_book);
   |                     ------ mutable borrow occurs here
...
27 |     observe_book(&*a_book);
   |                  ^^^^^^^^
   |                  |
   |                  immutable borrow occurs here
   |                  mutable borrow later used here

After thinking through what I had initially tried, I decided it made sense to separate the lifetimes of the mutable reference to a Book and the instance of Book itself. I then came up with this:

fn borrow_and_read<'a, 'b>(a_book: &'a mut Book<'b>) 
where 'b : 'a {
    match a_book.curr_page {
        Some(page) => a_book.curr_page = Some(page + 1),
        None => a_book.curr_page = Some(0),
    };
}

which does compile and output the expected results.

I am confused as to why my initial error message was that a_book was borrowed mutably more than once. I thought I would be ok passing around a single mutable reference since each usage of the reference understood that the reference was mutable. This thinking seems to be confirmed by the final implementation of my borrow_and_read function but I am not completely sure why specifying that the lifetime of the Book instance outlives the mutable reference with where 'b : 'a fixes my issue.

I am hoping to get a solid understanding of how using the same lifetime for both the mutable reference and Book instance yield the errors I got.

like image 511
AC-5 Avatar asked Oct 16 '22 06:10

AC-5


1 Answers

The problem with your original is that the lifetimes are too constrained. By making the borrow on the Book have the same length as the borrow on the book title ("The Book"), the mutable borrow is forced to last as long as the actual book itself, meaning that it can never be immutably borrowed.

Let's explore that. It'll be easier to examine your fixed version and then look at what the original does to constrain it.

fn borrow_and_read<'a, 'b>(a_book: &'a mut Book<'b>) 
where 'b : 'a {
    match a_book.curr_page {
        Some(page) => a_book.curr_page = Some(page + 1),
        None => a_book.curr_page = Some(0),
    };
}

This function has two lifetime parameters: one for the book itself and one for the mutable borrow on the book. We also constrain 'b: 'a, meaning that any borrows with lifetime 'a are valid for no longer than borrows with lifetime 'b. This is actually redundant, since the compiler can see that anyway. By having an argument with type &'a mut Book<'b>, 'a already can't last longer than 'b.

Now let's look at main. We'll call the lifetime on the book itself 'book. We'll call the lifetime on the mutable borrow of the book 'mtb. Finally, we'll call the immutable borrow (at observe_book) 'imb. Let's see how long each lifetime has to last.

// Initialize `the_book`. 'book has to start before this.

// Mutably borrow `the_book`. 'mtb has to start here.
let a_book: &mut Book = &mut the_book;

// Use the mutable borrow. 'mtb has to still be valid.
borrow_and_read(a_book);
// Use the mutable borrow. 'mtb has to still be valid.
borrow_and_read(a_book);

// Deref the mutable borrow and reborrow immutably.
// 'imb has to start here, so 'mtb has to end here.
// 'imb is a reference to `the_book`, so 'book has to still be active.
observe_book(&*a_book);

// The variables are no longer needed, so any outstanding lifetimes can end here
// That means 'imb and 'book end here.

So the crux of the issue here is that with this setup, 'mtb has to end before 'book. Now let's look at the original version of the function.

fn borrow_and_read<'a>(a_book: &'a mut Book<'a>) {
    match a_book.curr_page {
        Some(page) => a_book.curr_page = Some(page + 1),
        None => a_book.curr_page = Some(0),
    };
}

Now we only have one lifetime parameter, which forces the lifetime of the title and the lifetime of the mutable borrow to be the same. That means that 'mtb and 'book have to be the same. But we just showed that 'mtb has to end before 'book! So with that contradiction, the compiler gives us an error. I don't know the technical details of why the error is cannot borrow*a_bookas mutable more than once at a time, but I imagine that the compiler thinks of "usages" of a variable similarly to how we talk about lifetimes. Since 'book has to last until the call to observe_book and 'mtb is the same as 'book, it treats the usage of 'book as a usage of the mutable borrow. Again, I'm not completely sure about that. It might be worth filing an issue to see if the message can be improved.


I did actually lie a little bit above. While Rust doesn't do implicit type coercion, it does do lifetime coercion. Borrows with longer lifetimes can be coerced to borrows with shorter lifetimes. That ultimately doesn't matter too much here, but it's worth knowing about.

The title of the book, a string literal, has the type &'static str, where 'static is a special lifetime that lasts for the whole duration of the program. The data is embedded into the binary of the program itself. When we initialize the_book, it could have the type Book<'static>, but it could equally be coerced to Book<'book> for some shorter lifetime 'book. When we take the mutable borrow we're forced to have 'book: 'mtb, but we still have no other constraints.

When we call the one-parameter version of borrow_and_read, 'book and 'mtb have to both be coerced down to a shorter, common lifetime. (in this case, since 'book: 'mtb, 'mtb would work - and indeed, it's the longest lifetime that would work). With the two-parameter version, no coercion is necessary. 'book and 'mtb can be used as is.

Now when we deref a_book and reborrow it immutably, no mutable borrows can be active. That means that mtb and the shorter lifetime that both 'book and 'mtb were coerced to have to end. But a_book has lifetime 'book and we're using it, so 'book can't end. Hence the error.

With the two-parameter version, 'book wasn't coerced to a shorter lifetime, so it could continue.

like image 76
SCappella Avatar answered Oct 18 '22 19:10

SCappella