Search code examples
rustclosuresmutability

How to change the variable from inside Fn closure in Rust?


I have the following code (playground):

struct A {
    pub vec: Vec<u64>,
}

impl A {
    fn perform_for_all<F: Fn(&mut u64)>(&mut self, f: F) {
        for mut i in &mut self.vec {
            f(i);
        }
    }
}
fn main() {
    let mut a = A {
        vec: vec![1, 3, 44, 2, 4, 5, 6],
    };

    let mut done = false;

    a.perform_for_all(|v| {
        println!("value: {:?}", v);
        done = true;
    });

    if !done {
        a.perform_for_all(|v| {
            println!("value {:?}", v);
        });
    }
}

The following errors occur:

error[E0594]: cannot assign to `done`, as it is a captured variable in a `Fn` closure
  --> src/main.rs:21:9
   |
21 |         done = true;
   |         ^^^^^^^^^^^ cannot assign
   |
help: consider changing this to accept closures that implement `FnMut`
  --> src/main.rs:19:23
   |
19 |       a.perform_for_all(|v| {
   |  _______________________^
20 | |         println!("value: {:?}", v);
21 | |         done = true;
22 | |     });
   | |_____^

I have a list of loaded objects and a list of objects in a database. I need a function that takes a closure and executes it on the loaded objects and if we don't have the objects in the list, execute it on a list of objects from the database.

That function looks like:

pub fn perform_for_match_with_mark<F>(&mut self, mark: MatchMark, f: F)
where
    F: Fn(&mut GameMatch),
{
    self.perform_for_all_matches(
        |m| {
            // runtime list
            if let Game::Match(ref mut gm) = *m {
                if gm.match_stamp().mark == mark {
                    f(gm);
                }
            }
        },
        None,
    );
    // if we have called `f` above - don't execute lines below.
    let tx = self.match_tx.clone();
    GamesDatabase::perform_for_match_with_mark(mark, |ms| {
        // database
        self.perform_for_all_matches(
            |m| {
                if let Game::Match(ref gm) = *m {
                    if gm.match_stamp().id == ms.id {
                        f(&mut GameMatch::new_with_match_stamp(
                            tx.clone(),
                            ms.clone(),
                            gm.needs_server_set,
                            gm.server_id,
                        ))
                    }
                }
            },
            None,
        );
    });
}

We have to operate on objects from the database only if we were unable to find them in runtime list. That is why I decided to make a variable which says "we already found these objects in the list, leave the database alone".


Solution

  • Change your perform_for_all function to use FnMut instead of Fn:

    fn perform_for_all<F>(&mut self, mut f: F)
    where
        F: FnMut(&mut u64),
    {
        for mut i in &mut self.vec {
            f(&mut i);
        }
    }
    

    As Peter said, there is some compiler magic going on.

    The signature for Fn::call is:

    extern "rust-call" fn call(&self, args: Args) -> Self::Output
    

    This takes an immutable reference to self, which is why you can't modify any of the captured variables.

    The signature for FnMut::call_mut lets you mutate variables because it takes &mut self:

    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output
    

    By changing your closure from Fn to FnMut, you allow it to modify its captured variables, given that the references you pass to it are mutable.