I ran across a strange lifetime issue invloving higher-order functions in my code; I've prepared a minimum working example here. The issue is that, when do_for_each_file
is called with a reference to do_something
, this causes a lifetime error (implementation of `FnMut` is not general enough
), however if do_something
is instead wrapped in a closure and a reference to this closure is passed it does not. The functional programmer in me wants to eta-reduce that closure out, but clearly it is doing something implicit here, I'm guessing to do with lifetime coercion. Any help elucidating exactly what is going on here would be much appreciated.
The lifetime in a function like the |p| do_something(p)
closure, or a manual fn foo(…: &Path) -> io::Result<()>
which is short for fn foo<'a>(…: &'a Path) -> io::Result<()>
– the lifetime 'a
here is something that’s sometimes called a “late-bound” lifetime parameter, meaning that foo
itself would be - as far as the type system is concerned - a single thing of a single function type that’s a function generic over the lifetime 'a
, similar to how the single higher-rank function-pointer type for<'a> fn(&'a Path) -> io::Result<()>
works.
On the other hand, with type generics (or non-“late-bound” lifetimes), e.g. some fn bar<T>(…: T) -> …
, now the expression bar
stands for nothing concrete yet at all, as far as the type-system is concerned; instead you’ll have to write bar::<some_concrete_type…>
, and that’s an expression of some concrete function type with signature fn(some_concrete_type…) -> …
. Of course this ::<…>
argument is commonly elided, so the difference becomes often unnoticeable.
Still this means that when writing bar::<TypeWithLivetime<'_>>
, this can only be an instantiation of bar
with a single lifetime parameter, and it doesn’t automatically re-generalize. As you noticed, the solution can be to wrap it in another layer of closure. (Which at least doesn’t have any run-time overhead, so it’s mostly a syntactic concern).
I’ll fully agree that this isn’t particularly nice – as someone with lots of functional programming knowledge myself.[1]
But lifetime parameters are tricky, because it’s easy to make the compiler unsound mishandling them, could be - I’d assume - possibly a reason why pre-existing type system designs including mechanisms for re-generalizing polymorphic function expressions like this might not simply have been adapted from other existing functional PLs. ↩︎
the argument of do_something
is path: impl AsRef<Path>
, which must be a single type, the function's signature is the same as
fn do_something<P: AsRef<Path>>(path: P) -> Result<()>;
in other words, it's "early bound", as @steffahn said.
you can make it work by changing the "shape" of the argument type, introducing a "late bound" lifetime:
fn do_something<'a, P: AsRef<Path>>(path: &'a P) -> Result<()>;
this lifetime can be, and usually is, elided. also, since Path
is a DST, so you also need to add ?Sized
to the bounds to make it accept &Path
, so the actual signature may look like this:
fn do_something<P: AsRef<Path> + ?Sized>(path: &P) -> Result<()>;
or equivalently, using "impl trait in argument position", as your original code does (note the extra parenthesis is needed for technical reasons):
fn do_something(path: &(impl AsRef<Path> + ?Sized)) -> Result<()>;
with this signature, you can get rid of the closure and pass the function directly to do_for_each_file()
.