Lifetime may not live long enough while implementing a trait

I am trying to implement the Visitor trait on top of Invokable (initially it was an Fn trait, but I am trying to simplify it. Why value argument does not outlive self? I cannot find this in the reference. Thanks

#[derive(Debug)]
struct Value(i32);

trait Invokable<T> {
    fn invoke(&self, value: T);
}

trait Visitor {
    fn emit(&self, value: &Value);
}

impl<'a, F> Visitor for F
where
    F: Invokable<&'a Value> + 'a,
{
    fn emit(&self, value: &Value) {
        self.invoke(value);
    }
}

If you want this to compile, you can do this:

impl<F> Visitor for F
where
    F: for<'a> Invokable<&'a Value>,
{
    fn emit(&self, value: &Value) {
        self.invoke(value);
    }
}

This is probably what you had before when using Fn traits, which have different lifetime inference rules. A trait bound like Fn(&Value) is equivalent to the explicit for<'a> Fn(&'a Value).

3 Likes

Yes, thank but what is the explanation?

It will work if I add lifetimes to Visitor (not shown). I am getting another error while trying to use the trait: implementation of Fn is not general enough, see below

#[derive(Debug)]
struct Value(i32);

trait Visitor {
    fn emit(&self, value: &Value);
}

impl<F> Visitor for F
where
    F: for<'a> Fn(&'a Value) -> (),
{
    fn emit(&self, value: &Value) {
        self.call((value,))
    }
}

fn parse(vis: &dyn Visitor) {
    vis.emit(&Value(1001));
}

fn foo() {
    parse(&|value| {
        // compiler error
	/// implementation of `Fn` is not general enough
    });
}

Exercise: implement Invokable<&'static Value> for i32 and see if you can figure out why you can't implement Visitor for i32 analogously to your generic implementation.

And/or try it with impl<'a> Invokable<&'a Value> for &'a str.


You can think of

impl<'a, F> Visitor for F
where
    F: Invokable<&'a Value> + 'a,

as "there exists a lifetime 'a and a type F such that F: Invokable<&'a Value>". Or roughly, "F has a method invoke that takes &'s Self for all lifetimes 's, and &'a Value (for the specific lifetime 'a that we said exists)."[1]

But if you look at the trait you're trying to implement:

trait Visitor {
    fn emit(&self, value: &Value);
}

This says, "to implement Visitor, define a function emit that takes &'s Self and &'v Value for all lifetimes 's and 'v".

So here's the mismatch:

// vvvvvvvvvv
   Exists<'a> ForAll<'s> fn(&'s Self, &'a Value) // what F::invoke does
   ForAll<'v> ForAll<'s> fn(&'s Self, &'v Value) // what F::emit needs

Since F::emit has to handle any input lifetime on the &'v Value, you need F to implement Invoke<&'v Value> for all 'v, not just one specific 'a, and that's what the for<'a> bound does: effectively changes the Exists<'a> to a ForAll<'a>.

impl<F> Visitor for F
where
    F: for<'a> Invokable<&'a Value>,

The technical name for such a bound is a higher-ranked trait bound, or HRTB.

Nit: there is no inference here, it's just straight-up syntactical sugar.[2]

AFAIK it's only mentioned in the reference in passing here.

lifetime arguments can be elided in function item, function pointer, and closure trait signatures.

Which I would agree isn't adequate to explain the higher-ranked connection (and fails to mention dyn [closure trait] types).


  1. also F: 'a which doesn't actually matter for the example. ↩︎

  2. With no opt-out, making it syntactical salt when you actually want a single, unnameable lifetime (which comes up in niche circumstances) ↩︎

3 Likes

That's a different problem: Rust's poor inference of higher-ranked closures. You can work around it in the example like so:

    // an annotation on an input arg with an elided lifetime
    // nudges the compiler to try to implement the `Fn` trait it a
    // higher-ranked way instead of for a specific inferred lifetime
    //             vvvvvv
    parse(&|value: &Value| {
    })

If the actual use case is more complicated, you might need a "funnel" you can pass the closure into directly.

fn funnel<F>(f: F) -> F
where
    F: Fn(&Value),
{
    f
}

fn foo() {
    parse(&funnel(|value| {
    }));
}

(The desired Fn annotation directly on an argument also nudges the compiler.)

2 Likes

This is an amazing explanation. Thanks for the time you put to explain it so well

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.