Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Incorrect closure type deduced despite move keyword #44336

Closed
vitalyd opened this issue Sep 5, 2017 · 12 comments
Closed

Incorrect closure type deduced despite move keyword #44336

vitalyd opened this issue Sep 5, 2017 · 12 comments
Labels
A-closures Area: closures (`|args| { .. }`) C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@vitalyd
Copy link

vitalyd commented Sep 5, 2017

fn main() {
     let my_text = Some(String::from("my_text"));
     let runner = move || {
         if let Some(s) = my_text {
              println!("Here it is: {}", s);
         }
     };
     runner();
}

This fails to compile on 1.20 (and a few earlier versions that I tried) with:

error[E0507]: cannot move out of captured outer variable in an `Fn` closure
 --> src/main.rs:6:26
  |
2 |     let my_text = Some(String::from("My text"));
  |         ------- captured outer variable
...
6 |         if let Some(s) = my_text {
  |                     -    ^^^^^^^ cannot move out of captured outer variable in an `Fn` closure
  |                     |
  |                     hint: to prevent move, use `ref s` or `ref mut s`

But this should be capturing my_text by move and generating an FnOnce shouldn't it?

This issue was originally brought up in https://users.rust-lang.org/t/moving-option-into-a-closure-by-binding-it-in-a-pattern-does-not-work-noob-question/12721

@dtolnay
Copy link
Member

dtolnay commented Sep 5, 2017

The workaround is to somehow make the move clearer. Either of the following work. But the compiler should be able to recognize this is a move without a workaround.

- if let Some(s) = my_text {
+ if let Some(s) = {my_text} {
- if let Some(s) = my_text {
+ let my_text1 = my_text;
+ if let Some(s) = my_text1 {

@vitalyd
Copy link
Author

vitalyd commented Sep 5, 2017

An even simpler example that doesn't exhibit the expected outcome is:

fn main() {
    let my_text = String::from("My text");
    let runner = move || {
        println!("{}", my_text);
    };
    runner();
    runner();
}

This happily compiles and prints twice. It's as-if the move keyword is entirely ignored or what's actually moving here is &String and not the String value, but that should be the behavior if a move keyword is not specified.

@ExpHP
Copy link
Contributor

ExpHP commented Sep 5, 2017

I believe match does not technically move its argument, since it must support ref patterns:

fn main() {
    let opt = Some(vec![2]);
    match opt {
        Some(ref inner) => println!("{:?}", inner),
        None => {},
    }
    println!("{:?}", opt); // no problem; the match only borrowed the contents
}

@vitalyd 's example appears to be unrelated. The string is moved into the closure just fine (try using the string itself afterwards), but the closure implements Fn because the body only needs to borrow the string.

fn main() {
     let vec = vec![1,2,3];
     let f = move || vec.len();
     f(); // fine
     f(); // fine (f implements FnMut)
     vec.len(); // ERROR: value used after move (it belongs to the closure now)
}

I'm not sure I would consider either to be incorrect behavior (though the first one is a bit jarring)

On second thought, the more I think about it, the more that the error in the OP bothers me. Why does it not behave like @vitalyd's example and just move the vec into the closure regardless? (silly me, the real issue isn't about it not moving the vec; which it is. It's about the fact that it's trying to generate an impl for Fn) (this is what I get for thinking out loud in text)

@dtolnay
Copy link
Member

dtolnay commented Sep 5, 2017

Even so, we can clearly tell after the closing brace of the match whether you are allowed to keep using that variable or whether it has been moved by the match. So we already know whether a particular match moves the input. That needs to translate into the right behavior for move closures.

@vitalyd
Copy link
Author

vitalyd commented Sep 5, 2017

@ExpHP, yeah there are 2 slightly different issues/examples here. The example from rust users should not be a compiler error. The String example I gave shouldn't allow calling runner() twice. Both boil down to, I think, an incorrect type of closure to be determined by the compiler. Essentially, the move keyword doesn't do what it says on the tin :).

@vitalyd
Copy link
Author

vitalyd commented Sep 5, 2017

Should also mention that it's important that move works here. Given Rust doesn't allow specifying capture lists nor capture style, users only have move to work with.

In the Option example here, the binding used in the capture is the value itself. If someone wants to move a reference, then they should create a '&Option<...>' binding outside the closure and then use it inside the closure - then I'd expect the reference to move, which is of course just a copy of the reference.

@cuviper
Copy link
Member

cuviper commented Sep 5, 2017

@vitalyd The move keyword changes how the environment is captured, but I don't see any reason why that closure couldn't still implement Fn. I think your simpler example is fine to allow calling runner() twice.

@vitalyd
Copy link
Author

vitalyd commented Sep 5, 2017

@cuviper, 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? Perhaps this is a case where lack of explicit capture semantics is problematic.

Also, I think the Option case is the more problematic one as that code should compile - I think that's what anyone looking at it would assume (unless they know of this gotcha).

@cuviper
Copy link
Member

cuviper commented Sep 5, 2017

Writing the OP this way still fails:

fn main() {
     let my_text = Some(String::from("my_text"));
     (move || {
         if let Some(s) = my_text {
              println!("Here it is: {}", s);
         }
     })();
}

But adding indirection marked with FnOnce succeeds:

fn call<F: FnOnce()>(f: F) { f() }
fn main() {
    let my_text = Some(String::from("my_text"));
    call(move || {
        if let Some(s) = my_text {
            println!("Here it is: {}", s);
        }
    });
}

I would expect you could even drop the move altogether, since it should be inferred to use move semantics, but that doesn't work even in my second example. It's OK without move if you use the {my_text} trick though.

@vitalyd
Copy link
Author

vitalyd commented Sep 5, 2017

Yeah, this wouldn't be the only case where a workaround is needed in Rust. But, I'll admit that I would've thought the original code compiled without issue. It's also very newbie unfriendly given how vanilla the case is.

@steveklabnik steveklabnik added the T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. label Sep 5, 2017
@aidanhs aidanhs added the A-closures Area: closures (`|args| { .. }`) label Sep 7, 2017
@TimNN TimNN added the C-bug Category: This is a bug. label Sep 17, 2017
@memoryruins
Copy link
Contributor

Triage: OP's example now compiles on both editions.
rustc: 1.32.0

@matthewjasper
Copy link
Contributor

The fix appears to have been in 1.23

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-closures Area: closures (`|args| { .. }`) C-bug Category: This is a bug. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

9 participants