Understanding the borrow checker when borrowing independent parts of a struct

Hi Rustaceans! This is my first post in this forum. I'm having trouble understanding why the borrow checker behaves in certain way. I know that this is a very common topic among Rust beginners, and I've found many similar questions, but none of them have helped me to properly understand the issue. So I decided to create a new question here. Let me explain:

I have some data that looks like this (oversimplifying):

struct Task {
    is_done: bool
}

struct Tasks {
    tasks: Vec<Task>,
    by_name: HashMap<String, usize>
}

The Tasks struct has some sort of index by_name, which, to avoid multiple ownership issues, uses indexes as values. These indexes point to a location in the tasks vector.

Now, I want to remove completed tasks from the index. My first instinct tells me to write something like this:

impl Tasks {
    fn remove_done(&mut self) {
        self.by_name.retain(|_k, v| !self.tasks[*v].is_done )
    }
}

This of course doesn't work. The borrow checker complains:

15 |         self.by_name.retain(|k, v| !self.tasks[*v].is_done )
   |         ^^^^^^^^^^^^^------^------^----^^^^^^^^^^^^^^^^^^^^
   |         |            |      |      |
   |         |            |      |      first borrow occurs due to use of `self` in closure
   |         |            |      immutable borrow occurs here
   |         |            immutable borrow later used by call
   |         mutable borrow occurs here

I completely understand why this is happening. The borrow rules are simple: you can have either many immutable references or a single mutable one. Here self.by_name.retain(...) borrows mutably, and self.tasks[i] borrows immutably. This is not allowed.

I found a way to workaround this: borrowing both the tasks and by_name independently, like this:

impl Tasks {
    fn clean_done(&mut self) {
        let by_name = &mut self.by_name;
        let tasks = &self.tasks;
        by_name.retain(|_k, v| !tasks[*v].is_done )
    }
}

And, to my surprise, this actually works! the borrow checker is happy with this. And this is what I'm having trouble understanding.

For me it looks like this part is violating the borrow checker rules:

        let by_name = &mut self.by_name;
        let tasks = &self.tasks;

I'm borrowing something from self both mutably and immutably at the same time. How is this possible? Maybe it's because even though these are parts of self, they are independent things, so borrowing doesn't count as borrowing self. But if that's the case, then why the first example didn't work? That was also referring to independent parts of the struct, but in that case the borrow checker thought that I was actually borrowing self.

I'm happy to have discovered this trick, because I can already think of many ways of applying it to deal with borrow checking issues. But I still can't understand why it works.

Thanks in advance,

Manuel.

This is correct. The reason the first example didn't work is because closures are currently only able to capture entire values, and are not able to differentiate between so-called disjoint borrows. In the future, Rust wants to allow the above code to compile - see RFC 2229.

6 Likes

Though not exactly what you ran into, you may be interested in this article on how to handle interprocedural conflicts.

2 Likes