Problem with lifetimes in closure return types

Given a struct with a lifetime parameter (essential: the problem disappears if the struct doesn't have a lifetime parameter):

struct Foo<'a> {
    datum: &'a u8,
}

and a method on that struct that returns a value derived from &self, but that doesn't refer to &self and so (AIUI) should be able to outlive it:

impl<'a> Foo<'a> {
    pub fn foo(&self) -> u8 {
        self.datum + 1
    }
}

I want to write a generic function that gets a Foo from somewhere and then applies a closure to it, returning the closure's return value:

fn apply_closure<T, F>(f: F) -> T
where
    F: for<'r> FnOnce(&'r Foo) -> T,
{
    let datum = 0u8;
    let foo = Foo { datum: &datum };
    f(&foo)
}

The Foo does not outlive the function (in my real-life code I'm creating it within apply_closure by talking to some hardware), but the closure return value can outlive that Foo.

This compiles:

fn good() {
    apply_closure(|f| f.foo());
}

but this does not:

fn bad() {
    apply_closure(Foo::foo);
}

which I do not understand. I also don't understand the compiler's error message, particularly where the "expected" and "found" types appear to be identical:

error[E0308]: mismatched types
  --> lifetime.rs:51:5
   |
51 |     apply_closure(Foo::foo);
   |     ^^^^^^^^^^^^^ lifetime mismatch
   |
   = note: expected type `for<'r> <for<'r> fn(&'r Foo<'_>) -> u8 {Foo::<'_>::foo} as FnOnce<(&'r Foo<'_>,)>>`
              found type `for<'r> <for<'r> fn(&'r Foo<'_>) -> u8 {Foo::<'_>::foo} as FnOnce<(&'r Foo<'_>,)>>`
note: the lifetime requirement is introduced here
  --> lifetime.rs:18:36
   |
18 |     F: for <'r> FnOnce(&'r Foo) -> T,
   |                                    ^

I think that I need to put some sort of lifetime specifier T to say that it can outlive the &Foo passed to the closure, but not sure. I've tried various permutations but haven't made the apply(Foo::foo) version compile.

Many thanks.

The error message is slightly better on nightly (though still not perfect)

   Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
  --> src/lib.rs:25:5
   |
25 |     apply_closure(Foo::foo);
   |     ^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
   |
   = note: expected trait `for<'r, 's> FnOnce<(&'r Foo<'s>,)>`
              found trait `for<'r> FnOnce<(&'r Foo<'_>,)>`
note: the lifetime requirement is introduced here
  --> src/lib.rs:13:8
   |
13 |     F: for<'r> FnOnce(&'r Foo) -> T,
   |        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error[E0308]: mismatched types
  --> src/lib.rs:25:5
   |
25 |     apply_closure(Foo::foo);
   |     ^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
   |
   = note: expected trait `for<'r, 's> FnOnce<(&'r Foo<'s>,)>`
              found trait `for<'r> FnOnce<(&'r Foo<'_>,)>`
note: the lifetime requirement is introduced here
  --> src/lib.rs:13:35
   |
13 |     F: for<'r> FnOnce(&'r Foo) -> T,
   |                                   ^

For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` due to 2 previous errors

The problem is that Foo::foo is short for Foo::<'_>::foo which has the function signature for<'r> fn(&'r Foo<'t>) for some fixed (inferred/unspecified) lifetime 't; i.e. the compiler will search for a single fixed lifetime 't to fill in to make it Foo::<'t>::foo, and that thing has the beforementioned signature.

On the other hand, F: for<'r> FnOnce(&'r Foo) -> T, or more properly written F: for<'r> FnOnce(&'r Foo<'_>) -> T falls into lifetime elision rules for Fn… bounds, and is short for F: for<'r, 's> FnOnce(&'r Foo<'s>) -> T, so it requires a single function/closure that is generic over both the 'r and 's lifetime.

Note that it is (unfortunately) generally not uncommon that |x| foo(x) [1] works in some places where foo doesn’t. So when running into weird problems with passing a function, always try eta-expanding into a closure first.


  1. i.e. |f| Foo::foo(f) would’ve worked, too, in your code ↩︎

4 Likes

Thank you; makes sense.

The problem is that Foo::foo ... has the function signature for<'r> fn(&'r Foo<'t>)
...
F:... is short for F: for<'r, 's> FnOnce(&'r Foo<'s>) -> T , so it requires a single function/closure that is generic over both the 'r and 's lifetime.

This means that we can have our desired call-site syntax by making a function that's generic over the Foo lifetime parameter:

impl<'a> Foo<'a> {
    // AIUI: for<'r> fn(&'r Foo<'a> -> u8
    pub fn foo(&self) -> u8 {
        self.datum + 1
    }

    // AIUI: for<'r, 's> fn(&'r Foo<'s> -> u8
    pub fn bar<'s>(foo: &Foo<'s>) -> u8 {
        foo.datum + 1
    }
}

apply_closure(Foo::foo);   // Doesn't compile (as before)
apply_closure(Foo::bar);   // Compiles

but then (obviously, albeit perhaps inconsistently):

let f: Foo = ...;
Foo::foo(&f);  // Compiles
Foo::bar(&f);  // Compiles
f.foo();       // Compiles
f.bar();       // Doesn't compile: "this is an associated function, not a method"

It seems that to have the both apply_closure(Foo::bar) and f.bar() syntax compile, we'd need to be able to express some sort of higher-kinded lifetime over impls:

// Not Rust
for<'a> impl Foo<'a> {
    pub fn bar(&self) -> u8 {
        self.datum + 1
    }
}

which I imagine either has some nasty hidden consequences or isn't worth implementing (since as you mentioned, you can always just write |f| f.foo() instead).