0

I have a History type which contains an initial state, a current state, and a list of closures with every change required to compute the current state from the initial state. These are applied with an .apply(...) method which takes a boxed closure, uses it to modify the current state, and adds it to the list. Because I want these to be deterministically reusable they are Fn, not FnMut or FnOnce.

struct History<State: Clone> {
    initial: State,
    current: State,
    updates: Vec<Box<dyn Fn(&mut State)>>,
}

impl<State: Clone> History<State> {
    fn apply(&mut self, update: Box<dyn Fn(&mut State)>) {
        update(&mut self.current);
        self.updates.push(update);
    }
}

I am currently taking closures as Box<dyn Fn(&mut State)>, and it works fine:

fn main() {
    let mut history = History::<usize> {
        initial: 0,
        current: 0,
        updates: vec![],
    };

    let delta = 10;
    history.apply(Box::new(move |mut value| *value += delta));

    println!("{}", history.current);
}
10

This got me thinking about whether it would be possible for a method to accept arbitrary unboxed closures by using impl Trait instead of dyn Trait. In this case, our method could box the closures itself, so the call site would become:

    history.apply(move |mut value| *value += delta);

(Please entertain the question of whether this is even possible, even if it's a bad idea in this case.)

I am imagining that each closure site is like a distinct data type, instantiated with the enclosed values each time it's used, so impl Trait could specialize the method for each implicit closure like it does for each explicit type. But I'm not sure whether Rust actually works like that.

However, when I try making the change in the code, I get a new lifetime error:

    fn apply(&mut self, update: impl Fn(&mut State)) {
        update(&mut self.current);
        self.updates.push(Box::new(update));
    }
error[E0310]: the parameter type `impl Fn(&mut State)` may not live long enough
  --> src/main.rs:10:27
   |
10 |         self.updates.push(Box::new(update));
   |                           ^^^^^^^^^^^^^^^^
   |
note: ...so that the type `impl Fn(&mut State)` will meet its required lifetime bounds
  --> src/main.rs:10:27
   |
10 |         self.updates.push(Box::new(update));
   |                           ^^^^^^^^^^^^^^^^

This confuses me. I'm not sure where there's a reference that could be going bad.

In my head, the entire closure state is now being moved into apply via the impl Fn parameter, then moved into a Box that is owned by self. But it's complaining that I can't move the contents to a box, because I have a potentially-stale reference, not just owned data? Where am I borrowing anything? Why didn't this occur when I boxed the closure directly in main instead of in apply?


Is it possible to use impl Fn to accept an (arbitrarily-sized) closure as an argument? If so, how?

Jeremy
  • 1
  • 85
  • 340
  • 366

1 Answers1

3

Can you use impl Fn to accept arbitrarily-sized closures as arguments?

Yes.

impl trait in argument position is the exact same as a generic. These are identical:

fn foo1(_: impl Fn(u8) -> i8) {}
fn foo2<F>(_: F)
where
    F: Fn(u8) -> i8,
{}

This is in fact the generally preferred way to accept a closure (or many other trait implementations) because it allows the compiler to monomorphize the result and avoids any unneeded indirection.


Compiling your code has this help text (which currently has some rendering glitches):

help: consider adding an explicit lifetime bound `impl Fn(&mut State): 'static`...
   |
8  |     fn apply(&mut self, update: impl Fn(&mut State): 'static +  {
   |                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

What it means to say is to add + 'static:

fn apply(&mut self, update: impl Fn(&mut State) + 'static)

This works.

See also:

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • Thanks Shep. The point that most caught me was that I thought adding `'static` meant I was somehow declaring that my data must live forever, but after reading [this answer](https://stackoverflow.com/a/40053649/1114) you linked, it finally clicked that I'm just saying that my data can't have any references that aren't themselves `'static`, which does make sense (I don't intend for it to have *any* references). – Jeremy Aug 09 '18 at 16:19