Moving Option into a closure by binding it in a pattern does not work (noob question)


#1

Hello,
This is my first post here. Sorry if I missed something. Just cannot figure out why this does not build:

fn main() {
    let my_text = Some(String::from("My text"));

    let runner = move || {
        //let o = my_text;
        if let Some(s) = my_text {
            println!("Here's it: {}", s);
        }
    };
    runner();
}

It says:

cannot move out of captured outer variable in an `Fn` closure

though the closure is explicitly marked as move.
If I uncomment the line before the if let and replace my_text with o in the next one, it builds and runs as expected, even without the explicit move marking.
Not very intuitive…
Thanks for the help!

Dmitry


#2

Looks like a Rust bug to me. If I uncomment your workaround and then try invoking the closure twice — runner(); runner(); — then I get a reasonable error that makes it clear what the compiler is thinking:

closure cannot be invoked more than once because it moves the variable my_text out of its environment

Without the workaround, it should still detect that the closure moves out of its environment, but evidently does not.

Unless someone tells me I am wrong, could you please file an issue to track this?


#3

Yeah, seems like a bug to me as well. Pretty vanilla case though, a bug in that seems surprising.


#4

It fails for many patterns where some of the content isn’t copy.
The compiler is lacking reasoning (probably should have it) to select correct type for the closure.
One alternate workaround is to use a brace expression {my_text}
move is just a hint to the compiler of what the default for anything enclosed should be. In your case (with workaround) even without the move my_text would get forced to be move.

A similar bug. (Put string last and it compiles.)

let b = Box::new((String::from("My text"), 2));
let (_s, _i) = *b;

#5

I think it’s more than just a hint - it’s a directive/override. It’s supposed to force the compiler to capture by move any values inside the closure, even if a capture-by-reference would be deduced by the compiler otherwise.

I think this is an order-of-operations issue, rather than being similar?


#6

Let’s see what the compiler team says.


#7

I think this is the most impressive, most vanilla, most home-run-looking probably-a-bug bug report that I’ve ever seen to have existed in every single rust release since 1.0. And it has no existing duplicate issues that I can find (though there has been much fuss in the past over another issue with the content of the error message).

Here’s a movie showing how the error has evolved over time.


#8

Nice movie! :slight_smile:

Not to jump ahead, but supposing it’s a bug - I don’t think it’s fixable as it would be backwards incompatible (i.e. some code that compiles today won’t compile anymore).


#9

Responding here since I don’t want to pollute the issue thread with too much discussion

@vitalyd wrote:
that’s one way to look at it, sure. The closure simply owns the String and you can call it multiple times. So how does one force FnOnce generation without syntactic hacks or extra verbiage?

I can’t see how anybody needs or benefits from the ability to force FnOnce generation (by which I assume you mean the ability to forcibly disable FnMut generation, since FnOnce is always generated).

On the other hand, your runner example is an example of:

  • A Fn closure…
  • that closes over non-Copy data…
  • and has a 'static lifetime.

This is huuuge. This lets you write entire classes of programs that simply would not be possible otherwise. Unless there’s a soundness issue hiding somewhere beneath the cracks, I can only hope that this is working exactly as designed.

…and come to think of it, I’m almost certain that I must have used this functionality deliberately at least once, probably trying to do a cartesian product with flat_map.


#10

Hm. Now I’m even more confused. Definitely didn’t expect my original question to unveil some sort of compiler bug.
The Rust book says:

"Closures can capture values from their environment in three ways, which directly map to the three ways a function can take a parameter: taking ownership, borrowing immutably, and borrowing mutably. These ways of capturing values are encoded in the three Fn traits as follows:

FnOnce takes ownership of the variables it captures from the environment and moves those variables into the closure when the closure is defined. Therefore, a FnOnce closure cannot be called more than once in the same context.
Fn borrows values from the environment immutably.
FnMut can change the environment since it mutably borrows values."

So as a Rust beginner, I assumed that the mapping is one to one. However, @vitalyd 's example shows that a closure might move the String but still be called twice ?
Not clear at all…


#11

You’re right. In hindsight, that String example wasn’t a good one, and in fact the compiler picking Fn is what the common case will be. But, I think this touches on a topic that’s been discussed in this forum before, which is the “magic” that goes on in the compiler when it decides how the environment is captured and what closure trait to implement. Now, it’s not really magic once you get familiar with it, but it does present cases where it may not do what you intended or what the code looks like on the surface. Since Rust doesn’t allow specifying capture lists nor capture types (probably for the better, certainly in common cases), move is all you really have for some cases. Then it inspects the body as well though and still decides on whether it’s Fn, FnOnce, or FnMut(this latter one being the more obvious of the 3, I think). This automaticness is both a blessing and a curse.

The Option case from the OP, however, is really subpar. I would’ve thought that code compiles if someone asked me. It’s also fairly unfriendly to newcomers because it’s vanilla looking yet doesn’t work. But, NLL is of the same cloth in that regard so these warts have company :slight_smile:


#12

Yes, I’m afraid I muddied the waters with that String example. Let me try to clarify.

The String is moved into the closure there - you couldn’t use it after the closure is defined. The question was whether the closure should implement Fn or FnOnce (FnMut is irrelevant because there’s no mutation).

The compiler picks Fn. The best way to think of this is if you defined your own struct that contains a String field and then moved a String into it while creating a value of that struct type. Once you have that struct, since the closure doesn’t call any methods on the String that consume it, it can keep being invoked over and over - just like the struct you would’ve written.

So the more curious case is your original Option one.


#13

Another wrench in the works for you is that they have super-trait relationships, Fn: FnMut and FnMut: FnOnce. Basically, any Fn can act like FnMut that mutates nothing, and any FnMut can act as FnOnce where it mutates only during that one call. Transitively, Fn implements FnOnce too.


#14

Hmmm. I don’t like the way the book describes it. I’d describe it like this:

They form a simple heirarchy:

          FnOnce   >  FnMut  >  Fn
      <------------           ------------>
   More implementors         Stronger guarantees
More power to writer         More power to caller

Closures automatically implement the most powerful trait (to the caller) they can

  • A closure always implements FnOnce.
  • A closure implements FnMut if and only if it can be called multiple times.
  • A closure implements Fn if and only if it can be called multiple times simultaneously.

Notice my usage of the word can. Capture semantics and Fn traits are independent. Specifying move changes the capture semantics, which may change what a closure can do—but that’s the extent of its impact on trait selection.

Callers (should) ask for the weakest trait they need

  • If you need to call it multiple times with reentrancy, ask for a Fn. (this is rare)
  • If you need to call it multiple times, ask for FnMut. (e.g. Iterator::map)
  • Otherwise, ask for FnOnce. (e.g. Option::map)

#15

Oh, that suddenly makes much more sense! Thank you very much!
Apparently, the move keyword only tells the compiler how a closure should capture the environment, but has nothing to do with which of Fn->FnMut->FnOnce traits it implements.
For instance, the following example builds fine:

fn say_hello() -> Box<FnMut()>
{
    let hello = String::from("Hello");
    Box::new(move || {
        //let hello = hello;
        println!("{}", hello)
    })
}

fn main() {
    say_hello()();
}

but it would not without the move keyword.
However, if I uncomment the first line of the closure, to try to move out of the captured environment, it fails with the message: “cannot move out of captured outer variable in an FnMut closure” similar to the original post.
Here, however, the type of the closure is hinted by the return type of the say_hello function. In the OP there is no such hint, so IMO the compiler should not try to implement the Fn trait.