FnOnce closure can be called multiple times

According to the rust book Closures: Anonymous Functions that Can Capture Their Environment - The Rust Programming Language

FnOnce consumes the variables it captures from its enclosing scope, known as the closure’s environment . To consume the captured variables, the closure must take ownership of these variables and move them into the closure when it is defined. The Once part of the name represents the fact that the closure can’t take ownership of the same variables more than once, so it can be called only once.

but we can call below closure multiples times. Any adjustment to the book needed?

fn main() {
    let s = String::from("test");
    
    let f = move || {
        println!("{}", s);
    };

    f();
    f();
}

(Playground)

Hi,

I guess the issue here is how the println! macros treats s. I guess even if you state move on the closure level s is always taken as a borrow by the macro and thus does not take ownership of it.

If you change your code to:

fn main() {
    let s = String::from("test");
    
    let f = move || {
        let s2 = s;
        println!("{}", s2);
    };

    f();
    f();
}

It's ensured that the closure takes ownership of s as it is assigned to s2, regardles of how it is treated by the println! macro. Now you get the expected error message:

error[E0382]: use of moved value: `f`
  --> src/main.rs:10:5
   |
9  |     f();
   |     --- `f` moved due to this call
10 |     f();
   |     ^ value used here after move
   |
note: closure cannot be invoked more than once because it moves the variable `s` out of its environment
  --> src/main.rs:5:18
   |
5  |         let s2 = s;
   |                  ^
note: this value implements `FnOnce`, which causes it to be moved when called
2 Likes

As a rule of thumb, since you can call println!("{}", s) multiple times, the same applies to the closure. This closure is actually an Fn closure. Another thing to note is that every Fn closure is also an FnOnce closure, but as long as the compiler still knows that it's an Fn closure, i. e. as long as you haven't e. g. passed it to a generic function that just expects an FnOnce and you're now inside of that generic function's body, you will be able to call it multiple times.

println is a macro and it defeats the usual pattern that if you "pass" a variable directly then it would be consumed. It operates more like as if you called a &self method on s (that's actually exactly what it expands to).

One last thing to note is the move keyword. For this question, the only thing thats important about that one is that it has no effect in whether a closure is Fn or FnMut or not. It will not influence whether you can call the closure multiple times.

5 Likes

The closure does take ownership of s, moving it into the closure's anonymous struct. The body of the closure, however, doesn't consume its owned version of s, so can be called multiple times.

2 Likes

Hmm.. this error message is actually pretty bad in some sense since it doesn't explain the details correctly. Every closure implements FnOnce. And a move closure will always move s out of its environment, depending on what you understand under the term "environment".

3 Likes

If I add let ss = s between those two f(), the compiler will complain:

2 |     let s = String::from("test");
  |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
3 |     
4 |     let f = move || {
  |             ------- value moved into closure here
5 |         println!("{}", s);
  |                        - variable moved due to use in closure
...
9 |     let ss = s;
  |              ^ value used here after move

Yes. And with the move keyword as you have it there, it will even complain if you put that let after the calls to f, something that does compile without the move.

As another rule of thumb, move is really only used if you either return a closure from a function or if you pass a closure to something like thread::spawn where it gets executed in the background potentially for longer than your current function.

1 Like

Here's an expanded example that demonstrates some of the additional behaviors we've talked about:

fn example()->impl FnOnce() {
    let s = String::from("test");
    
    // Removing the `move` keyword is a compile error:
    // Without it, `f` contains a reference to a local variable and cannot be 
    // returned
    let f = move || {
        println!("{}", s);
    };

    // This line is an error because `s` has been moved into the closure
    // dbg!(s);

    // Here, the compiler knows the actual value of `f` can be called through
    // the `Fn` trait, so the closure can be called multiple times.
    f();
    f();
    
    f
}

fn main() {
    let f = example();
    
    // The closure is callable here, but ...
    f();
    
    // This call is illegal: `example` returns `impl FnOnce()`,
    // so it is illegal to call `f` multiple times, even though
    // the implementation could allow it.
    // f();
}

(Playground)

Well, thats interesting, though - I was not aware of this :blush:

so in below case, the compiler infer f() can be called as FnMut?

struct MyStruct(String);

impl Drop for MyStruct {
    fn drop(&mut self) {
        println!("droping: {}", self.0);
    }
}

fn main() {
    let mut s = MyStruct(String::from("test"));
    
    let mut f = move || {
        s.0.push('1');
    };

    f();
    // let ss = s; <- won't work
    f();
}

Yes; the line

let mut f = move || {
    s.0.push('1');
};

gets compiled into something vaguely like this:

struct Closure {
    s: MyStruct
}

impl FnMut() for Closure {
    fn call_mut(&mut self) {
        self.s.0.push('1');
    }
}

let mut f = Closure { s };

Each call to f() then modifies the MyStruct stored inside the closure. When the closure itself is dropped at the end of main, it calls the drop() method of Closure.s the same as any other structure would.

2 Likes

So move only affects ownership but not the number of times callable for the closure?

1 Like

Without move, the desugared code is like this (contrast to my post above):

struct Closure<'a> {
    s: &'a mut MyStruct
}

impl<'a> FnMut() for Closure<'a> {
    fn call_mut(&mut self) {
        self.s.0.push('1');
    }
}

let mut f = Closure { s: &mut s };
2 Likes

This is correct - Fn, FnMut or FnOnce is a concern that is independent of what the closure owns or not in its captures.

If you would like to learn how closures operate under the hood, check out my blog post on them

4 Likes

I confirm that @RustyYato's blog post is a must read in order to better understand what the environment captured by a closure is, and how then a closure is called / how its environment is accessed (signature of the "closure" "method" / () call operator).

What we know about f:

  • it "captures s" in some form (i.e., by value, by exclusive reference, or by shared reference).

  • The move annotation on the closure forces all captures to be by value.

    • (otherwise each capture is as minimal as possible: if the body of the closure only uses shared-reference-based APIs, then s would be captured by reference, otherwise if a capture by exclusive reference suffices, then it would be captured that way, otherwise it would be captured by value. In the case of || println!(…, s), only shared reference would be needed, so f would capture &s if it weren't for the move annotation).

That's it for the captures part. Now, let's see how these captures are used / consumed when the closure is called: is there &mut-based mutation on the captured environment? Is part of the captured environment dropped?

  • And in this example, again, only &-based APIs are used on the environment (s), so the closure is callable with just a shared reference to it: the type of f implements Fn(), and we could say that f() is sugar for call(&f). Thus, as with any &-based function / method call, f can be called multiple times.

  • If the body of the function does something like drop(s);, or any other action that moves1 out of s (such as let s2 = s; or let ss = s;), then, since s is not Copy (otherwise it would not be possible to move out of it), calling f consumes ownership of (part of) its environment, and thus, part of f itself: the type of f implements FnOnce only, and f() is sugar for call(f), which takes ownership of f, leading to f not being callable more than once.

To conclude this, and as kind of an exercise, it should now be clear what @steffahn said about a move annotation not being able to make a closure be "more FnOnce than it already is" (or to be more precise: a move annotation cannot make a closure cease to be FnMut or Fn): indeed, a closure ceases to be Fn{,Mut} based on what the body of the closure does.

  • move can affect what the environment of the closure is, how the locals are captured;

    • this is mainly useful when wanting to have non-local / potentially long-running closures, e.g., 'static closures.
  • the body of the closure affects how this environment is accessed, and thus, whether the closure, on top of FnOnce, gets to also have FnMut or Fn capabilities.

    • (and if a move annotation is missing, it will also affect how the locals are captured, by default)

Finally, know that Rust would be equally expressible if all the closures in Rust where always move-annotated, it would just be a bit more cumbersome to write sometimes.

  • Example:

    let mut s = String::new();
    iter.for_each({
        // closure called multiple times despite the `move`.
        move |item| write!(&mut s, "{}, ", item)
    });
    println!("{}", s); // Error, `s` moved in the previous closure
    

    Fix (while keeping the move):

    let mut s = String::new();
    iter.for_each({
        let s_ref_mut = &mut s; // 🤯 you can `move`-capture a `&mut` reference 
        move |item| write!(s_ref_mut, "{}, ", item)
    });
    println!("{}", s); // Ok, `s` wasn't moved, it was just `mut`-borrowed.
    

1 Even if you move out of a captured var and proceed to move something back into that var, the function nevertheless ceases to be Fn{,Mut} and is FnOnce only.

5 Likes