Move a boxed function inside a closure


#1

Hiya, I have this issue where I box up a function, but then can’t move it in a closure — it looks like it’s trying to move the function instead of the box. Is there a way to tell rust “please move the outer thing”?

playpen

#[derive(Debug)]
struct MyStruct;

type MyFunction = FnOnce(MyStruct) -> Result<MyStruct, ()> + Send;

fn main() {
    let functions: Vec<Box<MyFunction>> = vec![Box::new(|s| -> Result<MyStruct, ()> { Ok(s) })];
    let s = functions
        .into_iter()
        .fold(Ok(MyStruct), |result_s, boxed_f| {
            result_s.and_then(|s| boxed_f(s))
            // Error here:        ^^^^^^^ cannot move a value of type std::ops::FnOnce(..)
            // But shouldn't it be moving the box which is `Sized`, not the function inside the box?
        });

    print!("{:?}", s);
}

#2

The problem is related to FnOnce. I’ll try to explain.
FnOnce can used… yeah, once :smile:. To do so, it must be moved away, but this cannot be done with Box, just because you can only get the reference of the stored object.

I can think of two options:

  1. change FnOnce to fn or Fn (I think that FnMut can be used if you set boxed_f mutable)
  2. use FnBox, which unfortunately is unstable

Honestly, I don’t know if there is any way to use impl Trait in this case, but I think not.


#3

There’s currently no way to do this with a Box<FnOnce()> because the FnOnce trait call function takes the function itself by value rather than by reference, and to call that the function needs to be moved out of the box. But the FnOnce doesn’t have a static size, so it can’t be moved onto the stack.


#4

One workaround is to define your own FnBox - you’d lose closure call syntax sugar, but doesn’t seem like a big deal.


#5

Here’s an example for @vitalyd approach: https://github.com/rust-lang/cargo/blob/e2348c2db296ce33428933c3ab8786d5f3c54a2e/src/cargo/core/compiler/job.rs#L13-L25


#6

Yeah, this is how I’d have implemented it. But I don’t think the associated type vs generic param really matters here.


#7

Learn something new every day :slight_smile:, and the boxfnonce crate provides an implementation much like the ones mentioned.
Thank you all very much :bowing_man:!

Summary for future-me-who-will-forget-this:

  • FnOnce takes itself by value when you call it.
  • To pass a FnOnce around (transferring ownership), it must be boxed, since it’s not Sized.
  • A Box<FnOnce()> is a Sized type with a reference to the FnOnce
  • To call the function inside the Box<FnOnce()>, it must be moved out of the box because we need to own the FnOnce to be able to call it by value, whereas the Box only holds the reference.
  • To own it, we would have to move it onto the stack.
  • However, since it’s not Sized, we can’t do that.

To make this work, we can implement a trait for FnOnce with a trait function which takes Box<Self> and can invoke the FnOnce whilst inside the Box.


#8

I understand what you’re trying to say, but the above isn’t strictly true. If you were working with generics, you can pass F: FnOnce around by value unboxed. The trait type itself is unsized, like all traits. You’re running into an issue specifically because you’re using trait objects.


#9

Is it more accurate to say:

To pass a FnOnce around without monomorphization, it must be boxed into a trait object, since it’s not Sized.


#10

Sort of :slight_smile:. You can have trait objects via &FnOnce too, although it’s just as useless here because you can’t move out of a borrow. The issue also isn’t strictly about moving it around.

You can probably just leave this bit out - now that I think about it, I don’t think it’s adding any extra value.

Here’s how I’d summarize the issue:

  1. You want to store different FnOnce types in a homogeneous container (Vec<T>). You want the container to own these closures.
  2. You can’t store different closures in the Vec because each closure instance is a unique type.
  3. For that reason, you need to employ type erasure - aka trait objects in Rust.
  4. A (uniquely) owned trait object is done via Box - eg Box<FnOnce()>

At this point, you’re golden as far as storage (and moving things around) is concerned. The problem is in calling the closure:

  1. FnOnce, since it can only be called once (hence the name) takes self by value and consumes itself.
  2. A Box<FnOnce()>, to call the inner closure, needs to have it by value. As such, the enclosed closure has to be “pulled out”. This can’t happen because traits, on their own, are unsized - the box itself is adding size information by virtue of storing a fat pointer. So it’s a catch 22.
  3. Using FnBox works because the call works off self: Box<Self>, and thus doesn’t require pulling the innards out - the call goes through the box, and consumes the box in the process.

I’ll note that the fact Box<FnOnce()> isn’t callable is a language wart, that may one day be eradicated.