`move |...|` does not take by value?

I am a bit puzzled why Rust actually makes FnMut(i32) -> i32 out of this:

fn main() {
    let mut total: i32 = 0; // why is `mut` needed.

    let mut square_it = move |x: i32| {
        total += x * x;
        total + x
    };

    println!("{}, {}", square_it(10), total)
}

I thought mentally it translates into somthing like this:

struct SquareIt {
  total: i64
}

impl SquareIt {
  fn call(&mut self, x: i64) {
    self.total += x * x;
    x * x
  }
}

But it does not even though there is move which should just Copy because i32 is Copy.
What is wrong with my mental model?
The chapter Closures: Anonymous Functions that Capture Their Environment - The Rust Programming Language
almost describes that, but then only makes an example with a move type Vec, sadly, something to extend probably.

You can't change self.total via &self. FnMut is what makes it &mut self.

2 Likes

Sorry that was a typo.

I am confused: my model with SquareIt is correct, but it needs let mut total: i32 = 0;
which is confusing?

Why do I need to make it mut when total (which is moveed) implements Copy?

let mut is a bit of an awkward construct in Rust, it has no impact on codegen so it is really more of a lint than anything else. It's kind of like the inverse of const in C.

So my guess is that, when total is moved into the closure, it retains knowledge of its mutability from its pre-move binding.

4 Likes

In my (limited) understanding, you are moving the &mut total.

The let mut ... is to help readers quickly understand the distinction between giving 0 a useful name, versus what you're doing here by declaring a binding that will change value.
I recall criticism of this was that you don't need to mark a &mut T binding as let mut if you only mutate the inner T ("through" the reference) and never change the mutable reference itself.

I think your model is too accurate, along the lines of what @ryanavella said; the compiler is pretending the captured variable is "the same" as the locally declared one despite being moved,[1] for the sake of ... some programmer's intuition I guess?[2] But not every programmer's intuition.

No, they're capturing total by value. (It will be a copy, because i32: Copy.) That's what move does and that's why the println prints different numbers.


Not requiring let mut for things that are only mutated after being moved into a closure would actually help this issue, because you'd start to get "useless mut" warnings in many cases. But there are some who are pretty vehement about let mut being important; they'd probably hate the loosened requirement.


  1. as far as the let mut check goes anyway ↩︎

  2. And that's just a guess. ↩︎

4 Likes

When a closure captures a "variable" (named binding), the mut is simply inherited from the defining (outer) scope. This is merely a piece of syntactic sugar – otherwise, bad things would happen:

  • either there would be no way to declare mutable captures,
  • or every capture would need to be implicitly mutable (violating good practice and causing inconsistency with the rest of the language).
2 Likes

or every capture would need to be implicitly mutable (violating good practice and causing inconsistency with the rest of the language).

Arguably this is already an inconsistency, because moves into closures "inherit" the mut requirement, but other kinds of moves do not. This is a move followed by mutation, and it doesn't require mut:

let vec = vec![];
{ vec }.push(1);

Moves to function calls that mutate the argument also don't require mut:

fn mutator(mut vec: Vec<i32>) {}

fn main() {
   let vec = vec![];
   mutator(vec); // move to mut
}

Closures are special that they recycle the binding, and let mut is more about controlling the bindings than mutation.

5 Likes
  • And where's the contradiction? I explicitly wrote that this is syntax sugar (hence, it must do strictly more than a mere move – it's equivalent with a re-declaration). I sense no inconsistency – captures in a closure are not just about values, but about bindings as well.
  • How do you propose it should be made more consistent? I find the status quo perfectly clear and intuitive.

A closure notionally desugars to creating an instance of a type where the captures are fields. It would be consistent with other forms of moving to not require the mut on the captured-by-value variable. Example.

Rust doesn't have read-only fields, so the current behavior can't be a desugaring consistent with the notional desugaring of closures.[1]


  1. Rust will have somewhat read-only fields under RFC 3323, but that's not implemented; moreover, the tightest privacy scope is at the module level, so requiring the mut still can't correspond to a desugaring even under RFC 3323. ↩︎

4 Likes

This notion is too simplistic, then, or maybe the desugaring is being taken too literally. A type where all fields have the same mutability is clearly incapable of describing a scope where only some of the bindings are mutable.

It's clearly not the capture syntax that is at fault here.

As far as I know, it's the most accurate. Things are moved out borrowed when you create the closure and not when you call it, the closure is movable, etc. The mut binding requirement on captures is an exception.

Anyway, as I alluded to before, how well this jives with any given programmer probably strongly corelates to how they feel about let mut more generally. I don't find it an important requirement, but recognize others strongly disagree.

Thanks a lot for the discussion and insight.
I feel very much that it would help the language to have a "correct" mental model (with a struct) for closures, as they are already quite tricky to explain (Fn -> implements FnMut -> implements FnOnce etc....)

I think I can make sense of what @kornel highlighted:

let vec = vec![];
{ vec }.push(1); 

Here you move vec into a scope and move it out again and call push on a "temporary", no variable binding refers to this temporary -> thats why no mut on vec.

@paramagnetic Says the model is to accurate, but then what really happens when I use move and need let mut total ?

The "struct with captures in fields" mental model is how closures are mechanistically implemented. It is a code-generation-level model.

The "needs mut/doesn't need mut" distinction has nothing to do with that. It's a higher, type-checking-level question. The compiler could be changed now to not require mut at all, and nothing would break in the language.

5 Likes

@paramagnetic : Jeah understood. But maybe you could explain a bit in more detail.
What actually happens when total gets "moved" into the structure:

Is it that the "struct with captures in fields" gets instantiated as Closure{ t: total } and here you see the "move" of total. But I think that does not fully explain the mut on let mut total, because such an instantiation would not need any mut...

Do you have maybe an explanation on this?

There's two different interpretations of "what actually happens", and they have different answers.

One is the code generation answer: total becomes the initializer for something inside the closure's implementation struct. This would be the variant you've described as:

But that's merely explaining how the compiler generates code for a closure, not what it's modelling with that code generation.

The other answer is a language-level answer; the binding of total moves into the closure, and retains the same mut flag as used outside for the purposes of type checking. You can't move a binding any other way, so there's no "deeper model" here; this is a statement about how closures work.

You can bring this into the "struct with captures in fields" model by saying that your original square_it = move |x: i32| { … } is translated into something more like:

struct SquareIt {
  total: i64
}

impl SquareIt {
  fn call(&mut self, x: i64) -> i64{
    let total: &mut i64 = &mut self.total;
    *total += x * x;
    x * x
  }
}

In this translation, if you'd declared total as let total: i64 = 0; instead of using let mut, you'd get:

struct SquareIt {
  total: i64
}

impl SquareIt {
  fn call(&mut self, x: i64) -> i64{
    let total: &i64 = &self.total;
    *total += x * x;
    x * x
  }
}

which fails to compile.

But note that this is simply a model of what the compiler does to compile a closure - it's not actually what happens, but in some circumstances, it's useful to think about it this way because it clarifies behaviour. When the model doesn't help, however, it's OK to discard the model and replace it - it's only there to make understanding simpler.

1 Like

I don't think "what actually happens" is a meaningful question to ask in the context of analogies and abstractions. What actually-actually happens is the CPU shuffles bytes around. But that's not terribly helpful.

What you have to understand is two things:

  1. The desugaring as presented here (struct with captures as fields) need not literally be the way the compiler implements closures, but it's a close approximation. I'm not sure whether this is the case; to check that, you'd have to deeply familiarize yourself with the internals of the compiler. "Desugaring" is also a term loosely used for "lowering" or "codegen". When generating code for a particular language construct, closures in this instance, it is emphatically not the case that the compiler generates lower-level literal Rust code from other Rust code. The compiler has full freedom to lower a particular language construct into HIR and MIR and LLVM and ultimately machine code, without any obligation to defer more abstract constructs to less abstract, literal Rust constructs.
  2. Therefore, the requirement of putting mut is orthogonal to the desugaring. The requirement of mut is a language-level device. Closures "desugaring to structs" does not mean that there is a literal struct SomeClosure { field: T, other: U } somewhere. There isn't.
1 Like

Whether there's a contradiction it depends what you expect let mut to mean, and what it's supposed to be useful for.

If it tautologically means exactly what has been implemented, then it's impossible for it to have any contradiction. Its meaning is rather specific - whether you can reassign the binding or get a &mut reference through this specific named binding directly, excluding reborrows. Then mutating through a temporary value or another binding after move/assignment doesn't count by definition, because it's not let mut.

But if you try to view through it giving some broader assurances, like "can this object be mutated?" then it doesn't do that, because that's not the same as taking a &mut reference through one specific named binding.

If you look at it from perspective of moves, then not all moves are treated the same by let mut. Move into a function or a move into a struct is not treated the same as a move into a closure. It may be surprising to people who care about mutability of objects that let mut cares about bindings not objects behind them, so it's not the move that affects it, but change of the binding.

So my issue with let mut is that it does a pretty specific narrow thing, which is more specific than what people may want from a feature for controlling mutability.

When I say let mut sucks I get answers like "nooo, immutability is great and controlling mutability is super important", yeah, but let mut only kinda half does that.

3 Likes

I'd like to call this out as really important; we are working with layer upon layer of models, where each layer is implemented in terms of what the layer below provides. The CPU actually-actually-actually causes changes in voltage levels, which cause bytes to shuffle around; and there's a layer below that in terms of the electric field effects that the CPU uses, which cause changes in voltage which cause bytes to shuffle around, which causes…

When you find that a model, such as "closures are implemented by moving values into a structure", breaks down, you need to consider the possibility that your model is incomplete, and you've just run into a case where the model no longer works.

1 Like

That was eventually what I was thinking, but isnt it a bit strange or inconsistent?
I guess its because the variable in the closure scope has the same name syntactically, and that makes creating a closure different from instantiating a struct with fields.

That let mut is a variable binding where mut denotes that the binding can be mutated and it is transitive (e.g Box)