How to avoid borrow checker errors when decomposing functions?

Hello.

Even though I'm new to Rust, I absolutely loved it for its safety, speed, meta-programming and other great features that other programming languages doesn't have.

However, there are certain problems in Rust I haven't had with other languages, particularly with the borrow checker.

I've always been trying to write a clean, readable code. Following the recommendations of Robert C. Martin from his book Clean Code, I always strive to decompose functions into smaller functions, into logically connected blocks of code. In other programming languages I had no problem with that, unlike in Rust.

Here is a simplified version of my code:

struct SomeType {
    list: Vec<u32>,
    integer: u32,
}

impl SomeType {
    fn new(list: Vec<u32>) -> Self {
        Self {
            list: list,
            integer: 0,
        }
    }

    fn update(&mut self) {
        for w in self.list.iter() {
            // SOME CODE

            // A LOT OF CODE ...
            // A LOT OF CODE ...
            // A LOT OF CODE ...

            self.integer += w;

            // A LOT OF CODE ...
            // A LOT OF CODE ...
            // A LOT OF CODE ...

            // SOME CODE
        }
    }
}

fn main() {
    let mut t = SomeType::new(vec![1, 2, 3, 4]);
    t.update();
}

I intended to extract a method subfunction from the update function to make the code a little bit cleaner.
So now the code looks like this:

    fn update(&mut self) {
        for w in self.list.iter() {
            // SOME CODE

            self.subfunction(*w);

            // SOME CODE
        }
    }

    fn subfunction(&mut self, w: u32) {
        // A LOT OF CODE ...
        // A LOT OF CODE ...
        // A LOT OF CODE ...

        self.integer += w;

        // A LOT OF CODE ...
        // A LOT OF CODE ...
        // A LOT OF CODE ...
    }

But after this seemed to be usual operation, the code doesn't compile anymore, for the following reason:

error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
  --> src/bin/test1.rs:16:13
   |
15 |         for w in self.list.iter() {
   |                  ----------------
   |                  |
   |                  immutable borrow occurs here
   |                  immutable borrow later used here
16 |             self.subfunction(*w);
   |             ^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

Probably, I understand why it happens. The borrow checker doesn't allow me to call the subfunction function, because otherwise the subfunction could potentially modify the list field, while it is being iterated in the update function, potentially breaking an invariant in the Vec type.

While it is an absolutely correct restriction by the borrow checker to prevent any potential errors, sometimes it may be annoying that even a function decomposition can break the code, while semantically the code does the same thing.

How do you deal with such restrictions by the borrow checker, particularly when decomposing functions?
What do you think would be a better way to decompose the update function in my example preserving all the safety rules without compiler errors, and preferably without any runtime bloat like RefCell?

Thank you!

I suggest reading this blog post, which explores a few approaches to handle this situation. Taking the example perhaps-too-literally, I'd suggest factoring approach so that subfunction was called on self.integer and not self.

Macros can mitigate some DRY aspects of inlining.

(RFC 2229 has stabilized.)

Pedantic nit:

It's deeper than potential errors; it is undefined behavior to have data behind a &mut observable by something else (it's an exclusive reference) or to have data behind a & change without an UnsafeCell barrier. Compiler optimizations and unsafe code can rely on those guarantees; they are part of the semantics of your program. The decomposition using a method that takes &mut self did change the semantics of the code.

(I.e. if you force the issue with unsafe, your program will hit UB.)

5 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.