Still confused by "move ||" vs "|: |" closures


#1

(Note: I’m using myself as a case study of an experienced C programmer who’s been coming to grips with the novel aspects of Rust as a way to point out ways in which the documentation could be improved.)

After going over the reference, the guide, the API docs and blog posts, I’m still unclear about exactly what the ownership implications of move |...| closures vs |:|. I’m clearly missing something in the explanations; they read as if they were the same thing.

Or is it that move means that the environment gets moved into the closure structure, and |: | means that the closure structure is moved as self into the closure function itself, consuming it? But I still don’t really understand what that means. Does it mean that the closure structure needs to be boxed in that case? Or is it up to spawn (for example) to box up all the nicely self-contained state so that it can be moved to a new thread?

Are there use cases for move |&: | / move |&mut: |, or does move always have to be used with FnOnce?

I think the docs could use one or two more clarifying examples - for example, for each type of closure, explicitly spelling out how the closure environment structure is defined, and how its passed to the corresponding closure.

Thanks.


#2

First, move means capture by value instead of by reference. That is, the values are moved into the closure and go where it goes. You can’t freely move around a non-move closure because it references the stack.

Basically, a move closure’s environment (or receiver as you’ll see below) looks like:

struct EnvMove {
    variable1: Type1,
    variable2: Type2,
}

and a non-move closure’s environment looks like:

struct EnvRef<'a> {
    variable1: &'a Type1, // Note: this will be `&'a mut` if an `&mut` reference to `variable1` is used in the closure.
    variable2: &'a Type2,
}

As for the Fn* traits, take a look at the signature of the call method in the respective traits: |:| -> FnOnce::call_once(self, ...) |&:| -> Fn::call(&self, ...) |&mut:| -> FnMut::call_mut(&mut self, ...)

The receiver, self, is the environment. A closure is just sugar for defining a struct to contain the environment (EnvRef or EnvOwn) and implementing one of the Fn traits on it.

Use cases:

  • Use FnOnce if you need to move variables out of the closure (consume the environment).
  • Use Fn if you need to reference the environment but don’t need to mutate it.
  • Use FnMut if you need to mutate the environment.

So you would use move |&:| / move |&mut:| if you want to be able to give away a closure that can be called multiple times. The limitation is that this closure can’t move anything out of itself.


#3

Right, that’s the language I find confusing - it makes it sound like the variables are moved into the closure, whereas that’s actually the job of move.

If you’re using move and you have a structure of the form:

struct EnvMove {
    variable1: Type1,
    variable2: Type2,
}

Isn’t that also consuming the environment? I think the distinction is between “consuming the closed-over variables themselves” (move) and “consuming the environment structure containing those variables” (FnOnce), but it’s easy to use imprecise language to gloss over the difference. Or am I wrong?

So that seems to imply that using move |&: | is pointless, because even though you’re moving the variables into the closure struct, the closure struct itself is still owned by the calling function (on its stack). Which means that the closure func can’t take ownership of the closed-over vars out of a borrowed struct, and their lifetime ends once the function returns and the struct constructed for that call dies.

Conversely, is there any use for a non-move FnOnce?

Is there scope for simplifying things by combining |: | and move? Or does that lose useful expressiveness?


#4

Something people always forget is that :/&:/&mut: are temporary annotations, which can go away once the compiler doesn’t need to know ahead of time which one it is (we’re getting closer and closer to that goal). They were never meant to make into 1.0, and hopefully they won’t.


#5

Exactly! The confusing part is that there are two environments: the stack where the closure is defined and the closures receiver (Env/EnvMove). move moves variables from the stack where the closure is being defined into the closure’s receiver (the closure object). When invoked, FnOnce moves the closure’s receiver into the closure’s call stack (unlike Fn and FnMut which put a reference on the call stack).

No! The variables are moved off the stack and into the receiver (the closure object). It’s just that this receiver isn’t moved into the closure’s call stack when it is invoked.

You can use FnOnce to guarantee that your closure will be called only once.


#6

Regardless of whether there are annotations, the Fn/FnMut/FnOnce distinction will still exist and still needs to be understood.


#7

Ah, I see.

let x = Foo();
let a = move |&:| { x.bar() };
// x now unusable
a();
a();

vs

let x = Foo();
let a = move |:| { x.bar() };
// x now unusable
a();
// a now unusable

vs

let mut x = Foo();
{
    let a = |:| { x.bar() };
    x.bar(); // x usable immutable
    a();
    // a now unusable (though still in scope)
}
// x is mutable

#8

Yep. That looks about right.


#9

Is a variable captured by a FnOnce usable after the closure is called? (I assume no if it is a moving closure and yes if it is not?) For example, using jsgf’s example:

EDIT: oh, so apparently both x and y are useable even though x was moved… Is that expected behaviour? (http://is.gd/ns3e6s)

EDIT2: Annd I think that it’s just Clone-ing x and y in my example because they’re i32s. Woops.


#10

Yeah, the “move means move unless its clonable” thing is a little confusing.


#11

Copying, not cloning. AFAIK, rust never implicitly clones.


#12

The key is that, for any type that implements Copy, when a value is moved, it moves an (implicit) copy of the value. This is true wherever moves occur (argument passing, assignment, closures, etc). In general we try to avoid the term “move” for this reason, but for closures we failed to find a more precise keyword. English seems to lack a term for “by-value assignment”.


#13

So why do we not call it Copying rather than Moving?


#14

Because “moving” is something that also happens for non-Copy types, where it does not produce a second copy of the value. (Instead, after the value is written to a new location, the original location becomes inaccessible.)