Book: Chapter 13.1 confusing explanation of Fn, FnOnce, and FnMut

Created on 27 Mar 2018  路  6Comments  路  Source: rust-lang/book

https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-01-closures.md#capturing-the-environment-with-closures

I think the explanation is a bit confusing. It states

All closures implement FnOnce because they can all be called at least once.

Is that actually true? Explanation of FnOnce says

FnOnce consumes the variables it captures from its enclosing scope, known as the closure鈥檚 environment. To consume the captured variables, the closure must take ownership of these variables and move them into the closure when it is defined.

So if every closure implements FnOnce, then every closure takes ownership and moves them. It can't be true.

Further

Closures that don鈥檛 move the captured variables also implement FnMut

Is that the only condition? If so, then from

closures that don鈥檛 need mutable access to the captured variables also implement Fn.

it follows that a closure can implement both Fn and FnMut. Oh, and FnOnce because every closure implements it.

I don't understand those particular traits yet, but I think this part can be clarified.

Most helpful comment

This seems to be a difficult subject. I would recommend to anyone that is having trouble understanding closures to thoroughly read this discussion.

My epiphany was the realization that closure can be depicted as a struct containing the closure environment. Variables of the enclosing scope are either moved into this structure(the closure) or this structure(the closure) contains references to those variables.

If this structure(the closure) contains references to the enclosing scope it cannot be moved independently since it has a reference to the caller stack (stack where it was defined). If you return from the function/method that defined the closure, you will be unable to use the closure outside of this environment since it would have a dangling reference. To circumvent this, one can use move in closure definition which means that closure takes ownership of the variables of the enclosing scope (they are moved into the closure struct) consequently making the closure independent of the caller stack.

You can see that the call of the FnOnce takes a self by value: FnOnce::call_once(self, ...). This indicates that the closure struct is consumed upon calling and the closure cannot be used/called anymore.

All 6 comments

It might be a bit confusing, but it's not incorrect.

So if every closure implements FnOnce, then every closure takes ownership and moves them. It can't be true.

You're forgetting that a Copy type, for example, can be moved by value. But it doesn't move, it Copies. &mut T can be taken by ownership too, as references are also values.

it follows that a closure can implement both Fn and FnMut.

Well, every FnMut implements Fn, but not every Fn implements FnMut.

Sigh. We've iterated a lot on this explanation, and apparently it's still not clear. Given that what we have isn't wrong and that making this substantially better may mean making large changes, I don't think we can fix this for print. I'm going to close this for now and tag for possible improvement in the next edition.

After reading the comments I am still confused. Could we have a clearer explanation in this thread?

I thought every closure implements Fn rather than FnOnce, because the following code won't compile:

    let mut bar = 1;
    let foo = || bar;
    bar = 2;
    println!("{}, {}", foo(), bar);

With the following error:

41 |     let foo = || bar;
   |               -- --- borrow occurs due to use in closure
   |               |
   |               borrow of `bar` occurs here
42 |     bar = 2;
   |     ^^^^^^^ assignment to borrowed `bar` occurs here
43 |     println!("{}, {}", foo(), bar);
   |                        --- borrow later used here

So I think bar is borrowed rather than copied, thus I inferred that this closure implements Fn (borrows) not FnOnce (moves). So it seems FnOnce is not implemented but Fn is implemented. And I concluded every closure implemented Fn rather than FnOnce.

This seems to be a difficult subject. I would recommend to anyone that is having trouble understanding closures to thoroughly read this discussion.

My epiphany was the realization that closure can be depicted as a struct containing the closure environment. Variables of the enclosing scope are either moved into this structure(the closure) or this structure(the closure) contains references to those variables.

If this structure(the closure) contains references to the enclosing scope it cannot be moved independently since it has a reference to the caller stack (stack where it was defined). If you return from the function/method that defined the closure, you will be unable to use the closure outside of this environment since it would have a dangling reference. To circumvent this, one can use move in closure definition which means that closure takes ownership of the variables of the enclosing scope (they are moved into the closure struct) consequently making the closure independent of the caller stack.

You can see that the call of the FnOnce takes a self by value: FnOnce::call_once(self, ...). This indicates that the closure struct is consumed upon calling and the closure cannot be used/called anymore.

This article by Steve Donovan is helpful: Why Rust Closures are (Somewhat) Hard

Update: an even better article is Andrew Pritchard's Understanding Closures in Rust.

Future readers, be aware that some code snippets used to demonstrate how using immutable borrows after mutable borrows doesn't work are out-of-date. Since version 1.3.1 (edition 2018), Rust has implemented non-lexical lifetimes, which supercharges the borrow checker by making it more context-aware.

As an example, this code used to not compile in older versions:

let mut x = 1.0;
let mut change_x = || x = 2.0;
change_x();
println!("{}", x);

But it works now (version 1.48.0 at least).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

icanrealizeum picture icanrealizeum  路  3Comments

BedfordWest picture BedfordWest  路  4Comments

dlukes picture dlukes  路  3Comments

binarycrusader picture binarycrusader  路  4Comments

xStrom picture xStrom  路  3Comments