Why mutation of captured by move variable is considered mutation?

In this code (playground):

fn main() {
    let mut x = 2;
    let mut y = move || {
        x += 1;
        x
    };
    let (a, b) = (y(), y());
    println!("{a} {b} {x}");
}

3 4 2 will be printed, which shows that the original x is not mutated. But if you remove the mut from let mut x, you will get an error. Why?

1 Like

There is no fundamental reason it has to work this way for safety or anything. But there is no other place to mark the capture itself as mutable, so that's how the syntax was designed.

3 Likes

Removing the closure and moving the outer variable in by hand gives you:

let mut x = 2;
x += 1;
x

Without the mut it's clearly invalid:

let x = 2;
x += 1;
x

The mut in let mut y mark the closure (and so its fields) as mutable. I think forcing x to be mutable is misleading and inconsistent with the rest of the language.

1 Like

Removing the closure would work like this:

let mut x = 2;
let mut closure_state = x;
closure_state += 1;
closure_state

Which in it compiler will warn that mut can be removed.

1 Like

But what if you want closure_state_x to be mutable but closure_state_y to be immutable? Currently there is no way to declare a change of mutability upon capture so its just inhereted from the declaration.

That would make any variable moved from the outer scope instantly mutable, which may not be intended.

Edit: I see @sahnehaeubchen made a similar point. It's nice to be able to have mixed mutable and immutable variables referenced in the closure.

If you really don't want the outer x to be mutable, you can rebind it for the closure:

fn main() {
    let x = 2;
    let mut y = {
        let mut x = x;
        move || {
            x += 1;
            x
        }
    };
    let (a, b) = (y(), y());
    println!("{a} {b} {x}");
}

That technique is also useful when you want to mix some captures moved by value with others by reference -- then you can create explicit references and move those instead.

13 Likes

Just to add more:

This rebind technique is also very useful when using tokio::spawn(async move { t }), the inner Future cannot carry non-static lifetimes, so you need to move everything, and therefore .clone() everything.

There are two ways I usually do this:

let name = self.name.clone();
let rx = self.receiver.clone();

tokio::spawn(async move {
    // Use `name` and `rx`
});

and like this (similar to what cuviper showed, cloned in the inner scope):

tokio::spawn({
    let name = self.name.clone();
    let rx = self.receiver.clone();

    async move {
        // Use `name` and `rx`
    }
});

TLDR: if you see tokio:spawn (or Fut: 'static) prepare to move, and if you see move, prepare to rebind! :v

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.