Why Does the Compiler Infer FnOnce for This Rust Closure Instead of FnMut?

Hello, Rustaceans!

I'm puzzled by why the Rust compiler determines that a closure is FnOnce in the following code snippet, when I expect FnMut might be more appropriate:

struct Foo {
    x: i32,
}

fn main() {
    let mut x = Foo { x: 1 };
   
    let mut closure = move || {
        let mut nested_closure = move || {
            let y = &mut x;
            y.x += 1;
        };
        
        nested_closure();
        nested_closure(); // This works fine
    };
    
    closure();
    // closure(); // This line causes a compile-time error
}

FnMut also captures its environment, but unlike FnOnce , it does not necessarily consume the captured variables. Given that the closure modifies x through a mutable reference within the nested nested_closure , it seems it could fit the criteria for FnMut . However, the compiler infers it as FnOnce , meaning the closure can only be called once before it consumes x . Could someone please clarify why the compiler chooses FnOnce for this scenario instead of FnMut ?

Non-obviously, this moves x into nested_closure. Since nested_closure is then dropped at the end of the evaluation of closure, so is x. In order for this to work, it must be impossible for closure to run more than once, and FnOnce is the appropriate constraint for that.

Removing the move from nested_closure accomplishes what you want. The outer closure becomes FnMut, instead, and the inner nested_closure does not take ownership of x. You can also avoid moving x by moving y, instead:

    let mut closure = move || {
        let y = &mut x;
        let mut nested_closure = move || {
            y.x += 1;
        };
        
        nested_closure();
        nested_closure(); // This works fine
    };
5 Likes

Interestingly, the error message becomes better the more clear you make your demand for the closure to be FnMut.

If it’s just calling multiple times, you get

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

which points to the point where x is determined to be moved, but is still suboptimal. The help message

note: closure cannot be invoked more than once because it moves the variable `x` out of its environment
  --> src/main.rs:10:26
   |
10 |             let y = &mut x;
   |                          ^

doesn’t make all that clear that we are still talking about closure here, not nested_closure, and it also doesn’t point out the move of nested_closure that contributes to this mention of x resulting in a move.


If you instead require FnMut explicitly such as in

struct Foo {
    x: i32,
}

fn fn_mut(_: impl FnMut()) {}

fn main() {
    let mut x = Foo { x: 1 };

    let mut closure = move || {
        let mut nested_closure = move || {
            let y = &mut x;
            y.x += 1;
        };

        nested_closure();
        nested_closure(); // This works fine
    };

    fn_mut(closure);
}

it’s a bit better:

error[E0525]: expected a closure that implements the `FnMut` trait, but this closure only implements `FnOnce`
  --> src/main.rs:10:23
   |
10 |     let mut closure = move || {
   |                       ^^^^^^^ this closure implements `FnOnce`, not `FnMut`
11 |         let mut nested_closure = move || {
12 |             let y = &mut x;
   |                          - closure is `FnOnce` because it moves the variable `x` out of its environment
...
20 |     fn_mut(closure);
   |     ------ ------- the requirement to implement `FnMut` derives from here
   |     |
   |     required by a bound introduced by this call
   |
note: required by a bound in `fn_mut`
  --> src/main.rs:5:19
   |
5  | fn fn_mut(_: impl FnMut()) {}
   |                   ^^^^^^^ required by this bound in `fn_mut`

now the help makes clear that it talks about the outer closure in the message where the occurrence of x is pointed to. Still not optimal though. The message is even better if the compiler no longer needs to infer FnMut based on the closure but is told explicitly by having the closure expression appear in a place that directly expects FnMut:

struct Foo {
    x: i32,
}

fn fn_mut(_: impl FnMut()) {}

fn main() {
    let mut x = Foo { x: 1 };

    fn_mut(move || {
        let mut nested_closure = move || {
            let y = &mut x;
            y.x += 1;
        };

        nested_closure();
        nested_closure(); // This works fine
    });
}

which gives the error

error[E0507]: cannot move out of `x`, a captured variable in an `FnMut` closure
  --> src/main.rs:11:34
   |
8  |     let mut x = Foo { x: 1 };
   |         ----- captured outer variable
9  |
10 |     fn_mut(move || {
   |            ------- captured by this `FnMut` closure
11 |         let mut nested_closure = move || {
   |                                  ^^^^^^^ `x` is moved here
12 |             let y = &mut x;
   |                          -
   |                          |
   |                          variable moved due to use in closure
   |                          move occurs because `x` has type `Foo`, which does not implement the `Copy` trait

only now does the error message point at all the relevant parts. The closure we are talking about, then the move statement on the inner closure (which is now considered the position where x is moved), and the place where x is mentioned (which is now only presented as the reason the inner closure expression moves x).


It’d assume based on this experience, that there could still reasonably be room for improvement of the error messages in the other two cases.

Note that these differences are not unreasonable, the versions of code really are increasingly explict, in that

  • the first version (unlike the other too ones) leaves it to the compiler to infer that having closure implement FnMut would be a desirable idea; but this is more of a side-thought limited to a note that wants to be concise on the whole “what if closure was supposed to be FnMut (or Fn), and why isn’t it, anyways” tangential
  • the second version is a bit more subtly still more implicit than the third; the approach here still is to leave it to the compiler to infer the kind of closure, and only demand the right kind of trait later. This is subtle, because this rule in Rust that closure traits are only not inferred in cases where the closure expression is directly passed to a closure-expecting function is a bit of a niche rule. None of this should matter that much for the error message though; I think here’s the most straightforward potential for improvement.
4 Likes

There's also yet another subtle bit of complexity here: rustc really wants it to be true that which traits a closure type implements is knowable exclusively from the body of a closure (and the types of its captures), importantly without doing any kind of borrow/move/lifetime analysis, in order to prevent cycles where the type of a closure depends on itself.

There's an explicit hack in the compiler to reverse closures' type inference if they're directly used as a function argument, and this is why the error message is so different — in that case the closure is FnMut and the body fails to satisfy that requirement, whereas in the let case the closure isn't FnMut and the bound created by the use isn't satisfied.

IIRC nobody is particularly pleased with this reality (and it's potentially partially inherited from when trailing closure arguments were specially syntactically privileged pre-1.0), but type inference can be quite fickle and without the hack a lot of things get meaningfully worse to use (as evidenced by the difference in error message quality here).

7 Likes

Thank you for the explanation! If I'm understanding correctly, when the inner closure moves x by reference, it essentially takes ownership of x without actually consuming it. Then, when this inner closure is dropped at the end of its execution, x is also dropped. Consequently, if the outer closure were to run again, it would lead to an error because x no longer exists. To prevent this scenario, the compiler enforces the FnOnce constraint on the outer closure, ensuring it can only execute once. Is that correct?

1 Like

Thank you for the detailed explanation and examples! It’s intriguing to see how the explicit requirement for FnMut can improve the clarity of the error messages.

From what you've shared, it appears that when the demand for FnMut is explicit, the compiler provides more precise error messages that help pinpoint exactly where the issue with the ownership or borrowing occurs. This specificity in the error messages is very helpful for understanding the constraints imposed by Rust’s ownership rules, especially for beginners like myself.

Broadly, yes, that's correct. In particular, your conclusions are exactly right.

I'd quibble about one thing, though. inner_closure doesn't move x "by reference." The compiler moves x because it is used, by name, inside the body of nested_closure, which is declared as move and therefore takes ownership of any variables it closes over.

inner_closure's body does also create a reference to x, but the move happens when inner_closure is created, not when it's run, whereas let y = &mut x is evaluated only when inner_closure is run. Even if closure never actually calls inner_closure, creating it the way your original code does would be enough to move x out of closure and into inner_closure.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.