illabout
illabout

Reputation: 3636

Why can't a variable be moved out of a closure?

I have the following function:

pub fn map_option<A: 'static, B: 'static> (a2b: Box<Fn(A) -> B>) -> Box<Fn(Option<A>) -> Option<B>> {
    Box::new(move |opt_a: Option<A>| {
        opt_a.map(|a| a2b(a))
    })
}

However, this was quite difficult to write. I started off with something simpler that didn't work, but I don't understand why it didn't work.

  1. Here is my first version:

    pub fn map_option_1<A: 'static, B: 'static> (a2b: Box<Fn(A) -> B>) -> Box<Fn(Option<A>) -> Option<B>> {
        Box::new(|opt_a: Option<A>| {
            opt_a.map(a2b)
        })
    }
    

    This gave me the following error:

    error[E0507]: cannot move out of `a2b`, a captured variable in an `Fn` closure
      --> src/lib.rs:11:19
       |
    9  | pub fn map_option_1<A: 'static, B: 'static> (a2b: Box<Fn(A) -> B>) -> Box<Fn(Option<A>) -> Option<B>> {
       |                                              --- captured outer variable
    10 |     Box::new(|opt_a: Option<A>| {
    11 |         opt_a.map(a2b)
       |                   ^^^ move occurs because `a2b` has type `std::boxed::Box<dyn std::ops::Fn(A) -> B>`, which does not implement the `Copy` trait
    
  2. I thought that I might need to move the closure, so that it gets ownership of a2b:

    pub fn map_option_2<A: 'static, B: 'static> (a2b: Box<Fn(A) -> B>) -> Box<Fn(Option<A>) -> Option<B>> {
        Box::new(move |opt_a: Option<A>| {
            opt_a.map(a2b)
        })
    }
    

    However, this also didn't work. It failed with the following message:

    error[E0507]: cannot move out of `a2b`, a captured variable in an `Fn` closure
      --> src/lib.rs:17:19
       |
    15 | pub fn map_option_2<A: 'static, B: 'static> (a2b: Box<Fn(A) -> B>) -> Box<Fn(Option<A>) -> Option<B>> {
       |                                              --- captured outer variable
    16 |     Box::new(move |opt_a: Option<A>| {
    17 |         opt_a.map(a2b)
       |                   ^^^ move occurs because `a2b` has type `std::boxed::Box<dyn std::ops::Fn(A) -> B>`, which does not implement the `Copy` trait
    

    This error message says that a2b doesn't implement Copy, which I guess makes sense, but I couldn't figure out how to fix it.

  3. Out of desperation, I tried the following:

    pub fn map_option_3<A: 'static, B: 'static> (a2b: Box<Fn(A) -> B>) -> Box<Fn(Option<A>) -> Option<B>> {
        Box::new(|opt_a: Option<A>| {
            opt_a.map(|a| a2b(a))
        })
    }
    

    This at least gave me a different error:

    error[E0373]: closure may outlive the current function, but it borrows `a2b`, which is owned by the current function
      --> src/lib.rs:22:14
       |
    22 |     Box::new(|opt_a: Option<A>| {
       |              ^^^^^^^^^^^^^^^^^^ may outlive borrowed value `a2b`
    23 |         opt_a.map(|a| a2b(a))
       |                       --- `a2b` is borrowed here
       |
    note: closure is returned here
      --> src/lib.rs:22:5
       |
    22 | /     Box::new(|opt_a: Option<A>| {
    23 | |         opt_a.map(|a| a2b(a))
    24 | |     })
       | |______^
    help: to force the closure to take ownership of `a2b` (and any other referenced variables), use the `move` keyword
       |
    22 |     Box::new(move |opt_a: Option<A>| {
       |              ^^^^^^^^^^^^^^^^^^^^^^^
    

    The problem with ownership makes sense, I guess. This is what led me to the solution above that actually works.

  4. One other thing I tried that didn't work is the following:

    pub fn map_option_4<A: 'static, B: 'static> (a2b: Box<Fn(A) -> B>) -> Box<Fn(Option<A>) -> Option<B>> {
        Box::new(|opt_a: Option<A>| {
            opt_a.map(move |a| a2b(a))
        })
    }
    

    This gave me the following error:

    error[E0507]: cannot move out of `a2b`, a captured variable in an `Fn` closure
      --> src/lib.rs:29:19
       |
    27 | pub fn map_option_4<A: 'static, B: 'static> (a2b: Box<Fn(A) -> B>) -> Box<Fn(Option<A>) -> Option<B>> {
       |                                              --- captured outer variable
    28 |     Box::new(|opt_a: Option<A>| {
    29 |         opt_a.map(move |a| a2b(a))
       |                   ^^^^^^^^ ---
       |                   |        |
       |                   |        move occurs because `a2b` has type `std::boxed::Box<dyn std::ops::Fn(A) -> B>`, which does not implement the `Copy` trait
       |                   |        move occurs due to use in closure
       |                   move out of `a2b` occurs here
    

Here's a playground with each of these functions.


I don't think I have a good enough understanding of lifetimes and ownership to see why each of these functions fail.

I can sort of understand how map_option_1 and map_option_3 fail, because move is not being used to explicitly move ownership, but I am surprised that map_option_2 and map_option_4 fail.

I am very surprised that map_option_2 doesn't work, but the actual map_option function works. To me, these are practically the same function.

Why do each of these map_option_X functions fail to compile??

Upvotes: 4

Views: 543

Answers (1)

Francis Gagn&#233;
Francis Gagn&#233;

Reputation: 65782

I thought that I might need to move the closure, so that it gets ownership of a2b

That's correct, you do need the move on the outer closure. Without move, the closure would capture a2b by reference. However, a2b is a local parameter, and returning a closure which has a reference to a local is invalid.

Adding move to the inner closure leads to an error because the function returns an Fn closure. For this argument, let's consider this map_option_5 function:

pub fn map_option_5<A: 'static, B: 'static> (a2b: Box<Fn(A) -> B>) -> Box<Fn(Option<A>) -> Option<B>> {
    Box::new(move |opt_a: Option<A>| {
        opt_a.map(move |a| a2b(a))
    })
}

If the inner closure captures a2b by value and the outer closure is also a move closure, then both closures end up capturing a2b by value. By the rules of ownership, only one of the closures can possibly own a2b at a time, so when the outer closure is called, it moves a2b out of itself (destructuring the outer closure) and into the inner closure (which is only possible for FnOnce closures, as they take self rather than &mut self or &self). The reason for the error message is that we are returning an Fn closure, not an FnOnce closure. We could indeed fix this by returning an FnOnce closure (but then it couldn't be called more than once):

pub fn map_option_5a<A: 'static, B: 'static> (a2b: Box<Fn(A) -> B>) -> Box<FnOnce(Option<A>) -> Option<B>> {
    Box::new(move |opt_a: Option<A>| {
        opt_a.map(move |a| a2b(a))
    })
}

Now, let's discuss why map_option works while map_option_2 doesn't. The problem stems from the fact that Option::map takes ownership of the closure argument. Thus, we end up in a situation similar to map_option_5 above. Option::map takes an FnOnce, because it only needs to call it at most once. Changing a2b to be a Box<FnOnce(A) -> B> wouldn't help though, because it can actually be used in many calls to map.

There's a way to avoid the inner closure: pass a reference to a2b to map. This works because

  1. Box<F> where F: Fn<A> implements Fn<A> and
  2. &F where F: Fn<A> implements FnOnce<A> (and also Fn<A>, though that's irrelevant here).
pub fn map_option_2a<A: 'static, B: 'static> (a2b: Box<Fn(A) -> B>) -> Box<Fn(Option<A>) -> Option<B>> {
    Box::new(move |opt_a: Option<A>| {
        opt_a.map(&a2b)
    })
}

The closure still takes ownership of a2b, but it doesn't consume it when it's called, so the closure can be called multiple times.

map_option works because its outer closure doesn't need to consume a2b. The inner closure captures a2b by reference from the outer closure. This works because calling an Fn closure only needs a shared reference to the closure.

Upvotes: 7

Related Questions