Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cannot borrow as mutable in a loop when calling a closure that borrows as immutable?

Here is the code:

fn test(){
    let mut numbers = vec![2];
    let f = || {
        for _ in numbers.iter(){
        }
        false
    };

    while false {
        let res = f();
        if res {
            numbers.push(10);
        }
    }
}

The error is:

   |
15 |     let f = || {
   |             -- immutable borrow occurs here
16 |         for _ in numbers.iter(){
   |                  ------- first borrow occurs due to use of `numbers` in closure
...
22 |         let res = f();
   |                   - immutable borrow later used here
23 |         if res {
24 |             numbers.push(10);
   |             ^^^^^^^^^^^^^^^^ mutable borrow occurs here

But if I change the while keyword to if, it can be compiled. How to fix this? I want to call the anonymous function in a loop.

like image 588
HYRY Avatar asked Aug 28 '19 06:08

HYRY


2 Answers

We can simplify your example even more by replacing the closure by a simple immutable reference

let mut numbers = vec![2];
let r = &numbers;

while false {
    println!("{:?}", r);
    numbers.push(10);
}

Here we get this error:

error[E0502]: cannot borrow `numbers` as mutable because it is also borrowed as immutable
 --> src/lib.rs:7:5
  |
3 | let r = &numbers;
  |         -------- immutable borrow occurs here
...
6 |     println!("{:?}", r); // use reference
  |                      - immutable borrow later used here
7 |     numbers.push(10);
  |     ^^^^^^^^^^^^^^^^ mutable borrow occurs here

And like in your example, replacing the while with if makes the error go away. Why?

You probably know about the important Rust rule: Aliasing nand mutability. It states that, at any given time, a value can either be borrowed immutably arbitrarily many times or mutably exactly once.

The statement numbers.push(10) borrows numbers mutably temporarily (just for the statement). But we also have r which is an immutable reference. In order for numbers.push(10) to work, the compiler has to make sure that no other borrow exists at that time. But there exists the reference r! This reference cannot exist at the same time as numbers.push(10) exists.

Let's see for the if case first:

let mut numbers = vec![2];
let r = &numbers;            // <------+      (creation of r)   
                             //        |
if false {                   //        |
    println!("{:?}", r);     // <------+      (last use of r)
    numbers.push(10);
}

While the lexical scope means the variable r is only dropped at the end of the function, due to non-lexical lifetimes, the compiler can see that the last use of r is in the println line. Then the compiler can mark r as "dead" after this line. And this in turn means, that there is no other borrow in the line numbers.push(10) and everything works out fine.

And for the loop case? Let's imagine the loop iterating three times:

let mut numbers = vec![2];
let r = &numbers;            // <------+      (creation of r)   
                             //        |
// First iteration           //        |
println!("{:?}", r);         //        |
numbers.push(10);            //        |  <== oh oh!
                             //        |
// Second iteration          //        |
println!("{:?}", r);         //        |
numbers.push(10);            //        |
                             //        |
// Third iteration           //        |
println!("{:?}", r);         // <------+     (last use of r)
numbers.push(10);

As can be seen here, the time in which r is active overlaps numbers.push(10) (except the last one). And as a result, the compiler will produce an error because this code violates the central Rust rule.

And the explanation is the same for your closure case: the closure borrows numbers immutably and f(); uses that closure. In the loop case, the compiler is not able to shrink the "alive time" of the closure enough to make sure it doesn't overlap the mutable borrow for push.


How to fix?

Well, you could pass numbers into the closure each time:

let mut numbers = vec![2];
let f = |numbers: &[i32]| {
    for _ in numbers.iter(){
    }
    false
};

while false {
    let res = f(&numbers);
    if res {
        numbers.push(10);
    }
}

This works because now, numbers is borrowed immutably also just temporarily for the f(&numbers); statement.

You can also use a RefCell as the other answer suggested, but that should be a last resort.

like image 71
Lukas Kalbertodt Avatar answered Nov 05 '22 06:11

Lukas Kalbertodt


It's not exactly sure what you're trying to accomplish, but one way to solve this, without changing your code too drastically, would be to use std::cell::RefCell (described in the std and in the book):

use std::cell::RefCell;

fn test(){
    let numbers = RefCell::new(vec![2]);
    let f = || {
        for _ in numbers.borrow().iter(){
        }
        false
    };

    while false {
        let res = f();
        if res {
            numbers.borrow_mut().push(10);
        }
    }
}

Here's a little bit tweaked demo, which actually does something:

use std::cell::RefCell;

fn main() {
    test();
}

fn test() {
    let numbers = RefCell::new(vec![0]);
    let f = || {
        for n in numbers.borrow().iter() {
            println!("In closure: {}", n);
        }
        println!();
        true
    };

    let mut i = 1;
    while i <= 3 {
        let res = f();
        if res {
            numbers.borrow_mut().push(i);
        }
        i += 1;
    }
    println!("End of test(): {:?}", numbers.borrow());
}

Output:

In closure: 0

In closure: 0
In closure: 1

In closure: 0
In closure: 1
In closure: 2

End of test(): [0, 1, 2, 3]

Rust Playground demo

like image 3
ruohola Avatar answered Nov 05 '22 07:11

ruohola