Closure: `value` moved, why `mut` needed?

fn main() {
    // `value` moved, why `mut` needed?
    let mut value = 0;
    // removing `mut` doesn't compile.

    let mut closure = move || {
        value += 1;
        println!("{}", value);
    };
    closure(); // 1
    closure(); // 2
    println!("{}", value); // 0
}
1 Like

I am no expert but probably because in the context that it is moved to it is mutated.

Because there's no other place to put mut on, and value binding is mutated.

8 Likes

Calling this closure mutates its captured environment, so calling it requires the FnMut trait, which takes a mutable reference to the closure itself. (The environment is stored within the closure value.)

2 Likes

That explains why closure needs to be marked mutable, but not why value needs to be marked mutable. But @kornel already answered that question.

Also see https://users.rust-lang.org/t/mut-binding-required-to-mutate-move-captured-var-in-a-closure/26825

I agree that requiring that the captured variable be mut makes no sense, although mut bindings in general are just a glorified-by-the-language lint, so it being required in a place where it should not is not that bad. But definitely an inconsitency of the language.

The rule should be that variables captured by a closure are always mut-bound, following anonymous bindings rules:

fn main ()
{
    let x = ::scopeguard::guard(27, |x| { dbg!(x); });
    *{x} += 42; // anonymous binding in `{x}` expression
}

That being said, it would remove the possibility for the programmer to state when captured variables / bindings should not be mutated1.

So it ends up being a case where the inconsistency gives programmers more "expressiveness", leading to a "it could be worse" accepted status quo.


1 at least for a FnMut closure: in the case of a FnOnce closure, bindings can be rebound within the body of the functionn so their mut-lint-ability can be changed / specified there.

3 Likes

Only the value binding captured by the closure is mutated. The value binding in the top-level main scope is not, as the last println! shows. After all, the following equivalent code does compile.

fn main() {
    let value = 0;

    let mut closure = {
        // Makes a mutable copy for the closure to capture.
        let mut value = value;
        move || {
            value += 1;
            println!("{}", value);
        }
    };
    closure(); // 1
    closure(); // 2
    println!("{}", value); // 0
}

In the end, it can be argued both ways and I don't know which one is the least surprising.

They are the same binding. The error is not that mut is required, but that the binding is still in scope. The fact that the value is Copy shouldn't matter as the binding is really what is being moved here.

So the code in the OP should not compile. The only way I see to fix this is to add a lint against it for now and consider forbidding it in the next edition.

2 Likes

I don't think so. It is the defining feature of Copy that bindings remain usable after being moved from.

Here is yet another equivalent code, desugaring the FnMut closure to a struct and an explicit &mut self method. In this case, no mut is needed on the value binding.

struct Closure(i32);

impl Closure {
    fn call(&mut self) {
        self.0 += 1;
        println!("{}", self.0);
    }
}

fn main() {
    let value = 0;

    let mut closure = Closure(value);
    closure.call(); // 1
    closure.call(); // 2
    println!("{}", value); // 0
}
1 Like

Please reread what I said carefully as I was very carefully in choosing my words. I'm not talking about a value being moved out a binding, I'm talking about a binding being moved out of a scope.

Also none of you code examples are equivalent because they all create new bindings. Yes they do compile to the same thing, but I'm talking about how theses read to a human. This is a ugly corner case and I only see one why to fixed.

I,as a human,interpreted it as being "moves ownership of the value into the closure",and am confused how bindings are a thing that could be moved(that is a concept I haven't seen in any language I looked at).

1 Like

@matt1985 In every language with closures, the closure captures the binding, not the value. That's why mutating value causes the outer value to change, and those changes persist even between closure calls. This is very standard.

The only unusual thing here is that it's still possible to access value after it has been moved into the closure. That's a side effect of Rust's Copy semantics.

Well,generally how I think about closures in garbage collected languages is that they have a reference to the stack frame where those variables were defined,allowing them to access all those variables even after the function returns. I know that this model is probably not accurate since that's not how stack frames work,and that it would keep a lot more objects around than necessary,but "bindings" never enter into the equation for me.

As a bit of rust intuition then, in general when you wind up with two muts, they are necessarily two different things. How you got there can be a riddle though.

@matt1985 There's many ways to implement closures. But when you're thinking about language concepts, you shouldn't be thinking about the implementation details (which can change), you should be thinking about them at a high level.

From a high level, regardless of the implementation, closures capture bindings. This is proven through how closures actually behave:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=cfa1286d13f4db7129a47a944974b1f2

When you call the make function, it creates a local value binding and then it returns a closure. This value binding is captured (in this case moved) into the closure. So whenever you call the closure, it will update that binding.

And as you can see, every time you call make it creates a distinct value binding, so foo and bar use different bindings for value.

It's important to note that bindings are not syntactic. If you look at the syntax you would think that foo and bar have the same value binding, since in the syntax there is only one let mut value = 0, but that's not what's actually happening.

Instead you get a new value binding every time make is called. But you don't get a new binding every time foo or bar is called, because it reuses the same value binding it had when it was created.


As a side note, Rust implements the above code similar to this. All captured bindings get passed into a struct, and the closure body gets turned into a method.

It gets even more interesting when the closure doesn't move the bindings. In that case, this code gets translated into something similar to this code. In other words, the closure captures a &mut reference to the binding, rather than moving the binding into the closure.

But from the programmer's perspective, the closure captures the value binding, and the exact way that Rust implements that isn't too important.

1 Like

I don't know why you felt the need to explain closures to me.

I just think that disallowing the use variables of Copy types after they were were used inside a move closure is unnecessarily restrictive. Eg:

let a=0_i32;
let func=move||drop(a);

println!("{}", a); // For some reason this wouldn't compile???

The previous code exemplifies my problem with the talk of "bindings that are moved",a concept I find totally counter-intuitive.

As an example for why bindings aren't ever moved here is this:

let mut a=String::from("hello");
let func=move||a;
a=String::from("world");
println!("{}",a);
println!("{}",func());

if the binding(rather than its value) was the thing that moved,then it wouldn't even be a valid name to refer to after the closure was constructed.

Essentially,when you use move closures,they create implicit bindings that the values of the variables it refer to get moved into.

1 Like

@matt1985 However, in this code you cannot use a after it is moved into the closure.

Your example only works because it is assigning to a, and NLL is smart enough to realize that you are not able to access the old value of a. This is easy to prove by slightly modifying your code. I'm curious whether your code would compile without NLL.

So, perhaps before NLL it was correct to say that bindings are moved into closures, but NLL has changed that. Very interesting. So it seems Rust has now deviated from how other languages treat captured bindings.

That code compiles in Rust 1.0 Compiler Explorer.

I don't understand what lifetimes have to do with it,since the closure doesn't borrow the variable,it moves its value into an implicitly created binding only usable in the closure(that's how I model what's happening).

Ìf you look at:

fn main() {
    let mut a=String::from("hello");
    let mut func=move|b:&str|{
        a=String::from("foo");
        assert_eq!(a,"foo");
        assert_eq!(b,"bar");
    };
    a=String::from("bar");
    func(&a);
}

The a inside and outside the closure are different variables of the same name.

1 Like

@matt1985 Hmm, I must have confused it with another aspect of NLL. Thanks!

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