Search code examples
rustclosurestraitslifetime

Difference between "Self" and elided lifetime


Don't know how to describe, but here is the minimal reproducing snippet (also on playground):

struct Ctx(Vec<Box<dyn Fn(&mut MockCtx)>>);

struct MockCtx<'a>(&'a mut Ctx);

impl MockCtx<'_> {
    // this works
    fn push<F: 'static + Fn(&mut MockCtx)>(&mut self, f: F) {
        self.0.0.push(Box::new(f));
    }
}

trait Push {
    fn push<F: 'static + Fn(&mut Self)>(&mut self, f: F);
}

impl Push for MockCtx<'_> {
    // fn push<F: 'static + Fn(&mut Self)>(&mut self, f: F) {    (1)
    fn push<F: 'static + Fn(&mut MockCtx)>(&mut self, f: F) { // (2)
        MockCtx::push(self, f)
    }
}

If I take (1), compiler reports

expected a `Fn<(&mut MockCtx<'_>,)>` closure, found `F`

and recommand to add explicit lifetime to restrict. If I do so or simply take (2), compiler reports

impl has stricter requirements than trait

The truth is I do not understand the problem from starting point... I understand what is lifetime and simple "outlive" rules, and to me compiler should have no worry about 'a for these code so far, because that can only be a thing when these Box<dyn Fn> actually get called.

Thanks for any replying and explaining!


Solution

  • This is understandably a little opaque, because several lifetimes are elided. It should become a little more clear if we write those out.

    Let's first look at the container:

    struct Ctx(Vec<Box<dyn Fn(&mut MockCtx)>>);
    

    This is short for:

    struct Ctx(Vec<Box<dyn for<'a, 'b> Fn(&'a mut MockCtx<'b>) + 'static>>);
    

    The 'static requires that any closure you put in the container must own its data. It cannot refer to other data that depends on a lifetime. The elided lifetime is 'static because Box has no lifetime parameters.

    The construct for<'a, 'b> Fn(&'a mut MockCtx<'b>) is a higher rank trait bound, or HRTB for short. This means that any closure you put in the container must be able to accept a mutable reference with any lifetime to a MockCtx<'_> with any lifetime parameter. Note for later that there are two lifetime parameters here.

    Now, let's go to the impl:

    impl MockCtx<'_> {
        fn push<F: 'static + Fn(&mut MockCtx)>(&mut self, f: F);
    }
    

    This is short for:

    impl MockCtx<'_> {
        fn push<F: 'static + for<'a, 'b> Fn(&'a mut MockCtx<'b>)>(&mut self, f: F);
    }
    

    The requirements on the closure are the same as for the container. No conflicts. But note that the closure must accept types other than Self. It must accept a mutable reference to a MockCtx<'_> with a different lifetime.

    Now let's go to the trait:

    trait Push {
        fn push<F: 'static + Fn(&mut Self)>(&mut self, f: F);
    }
    

    This is short for:

    trait Push {
        fn push<F: 'static + for<'a> Fn(&'a mut Self)>(&mut self, f: F);
    }
    

    This is going to conflict. A closure given to Push can only be called with a reference to Self, of any lifetime. But a closure that we store in our container must accept some types other than Self. There is a whole range of Self-like types that have different lifetime parameters that the closure needs to accept for our container.

    Now, we can explain why the two attempts failed. #1 fails because the closure in the trait and the trait impl can only accept Self, but we're attempting to store it in a container that holds closures that can accept MockCtx<'_> of any lifetime. #2 fails because the trait only requires the closure accept Self, but our trait impl tries to accept a more restrictive set of closures that can accept MockCtx<'_> for any lifetime.

    There are several ways to fix this, but they all amount to fixing this disagreement on the bounds for the argument of the closure.

    One way is to explicitly accept the full range of types in the closure bound. You have to mention types other than Self. So, for example, you can mention the concrete types:

    trait Push {
        fn push<F: 'static + Fn(&mut MockCtx<'_>)>(&mut self, f: F);
    }
    

    See here.

    Using a nightly feature of trait aliases, you could use a common name for all the requirements:

    trait MockFn = 'static + Fn(&mut MockCtx<'_>);
    trait Push {
        fn push<F: MockFn>(&mut self, f: F);
    }
    

    See here.

    Using a nightly feature of generic associated types, you could also make the trait slightly more general:

    trait Push {
        type SelfLike<'a>;
        fn push<F: 'static + Fn(&mut Self::SelfLike<'_>)>(&mut self, f: F);
    }
    

    See here.

    Alternatively, as @Jmb says in the comments, you can make the closure a generic parameter on the trait:

    trait Push<F> {
        fn push(&mut self, f: F);
    }
    

    See here.

    This is a relatively clean approach. It is asserted in the comments that this is useless, but I don't think it is.