Async function taking a reference - lifetimes problem

Let's say I have a function that loads a configuration:

use core::future::Future;

struct Config;

async fn load(path: &Path) -> io::Result<Config> {
    unimplemented!()
}

Now I'd like to abstract out the configuration loading piece in some other code, so I'd like to be able to pass a load implementation as a parameter:

async fn load_using<Fun, Fut>(load: Fun) -> io::Result<Config>
where
    Fun: Fn(&Path) -> Fut,
    Fut: Future<Output = io::Result<Config>>,
{
    let path = PathBuf::from("foo");
    load(&path).await
}

So far this compiles.

However, I cannot use it:

async fn test_1() {
    load_using(load).await;
}

async fn test_2() {
    load_using(|path: &Path| async { load(path).await }).await;
}
error[E0308]: mismatched types
   --> src/lib.rs:789:5
    |
789 |     load_with(load).await;
    |     ^^^^^^^^^^^^^^^ one type is more general than the other
    |
    = note: expected trait `for<'r> <for<'r> fn(&'r Path) -> impl for<'r> futures::Future<Output = std::result::Result<config::Config, std::io::Error>> {load} as FnOnce<(&'r Path,)>>`
               found trait `for<'r> <for<'r> fn(&'r Path) -> impl for<'r> futures::Future<Output = std::result::Result<config::Config, std::io::Error>> {load} as FnOnce<(&'r Path,)>>`
note: the lifetime requirement is introduced here
   --> src/lib.rs:780:23
    |
780 |     Fun: Fn(&Path) -> Fut,
    |                       ^^^

error: lifetime may not live long enough
   --> src/lib.rs:793:29
    |
793 |     load_with(|path: &Path| async { load(path).await }).await;
    |                      -    - ^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
    |                      |    |
    |                      |    return type of closure `impl futures::Future<Output = std::result::Result<config::Config, std::io::Error>>` contains a lifetime `'2`
    |                      let's call the lifetime of this reference `'1`

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

I guess the problem is that the compiler assumes that my Path reference will live only for the lifetime of the lambda call, but not for the lifetime of the future returned from the lambda. If I force the lambda to take ownership by cloning the data, it works:

async fn test_3() {
    load_with(|path: &Path| {
        let path = path.to_path_buf();
        async move { load(&path).await }
    }).await;
}

But if this is the only way out, then using reference makes no sense and I could just as well accept PathBuf as the input.

Is there any way to do that without cloning?
Basically I'd like to be able to tell the compiler that the &Path passed to the load_with function will always live at least as long as any future created by the load function.

1 Like

The generic code does not say that Fut result may contain a temporary reference given to Fun callback. Without a lifetime annotation it's assumed to be independent and unrestricted, and therefore couldn't safely hold on to an argument valid only within a specific scope.

The crappy part is that AFAIK there's no syntax to express it.

You need this to declare a temporary lifetime for the argument:

    Fun: for<'a> Fn(&'a Path) -> Fut,

and this to declare that the returned future is related to it:

    Fut: Future<Output = io::Result<Config>> + 'a,

but sadly they just happen to be two separate where clauses, and there's no syntax to make that 'a the same thing for both of them, unless you change the return type to be boxed:

Fun: for<'a> Fn(&'a Path) -> Pin<Box<dyn Future<Output = io::Result<Config>> + 'a>>,
1 Like

The crappy part is that AFAIK there's no syntax to express it.

Ok, that explains why all my attempts at expressing that failed (I haven't posted all the things I tried).
Are there any plans to remove that limitation in the future?

E.g. why can't I write:

    Fun: for<'a> Fn(&'a Path) -> (Fut + 'a),

Or maybe:

   Fun: for<'a> Fn(&'a Path) -> Fut<'a>,
   Fut<'a>: Future<Output = io::Result<Config>> + 'a,

Because Fut is a specific type with the lifetimes on it fixed.

You could imagine this existing in the future, yes.

2 Likes

There's actually a trick with helper traits:

async fn load_using<Fun>(load: Fun) -> io::Result<Config>
where
    Fun: for<'a> AsyncPathFn<'a, io::Result<Config>>,
{
    let path = PathBuf::from("foo");
    load(&path).await
}

trait AsyncPathFn<'a, Out>
where
    Self: Fn(&'a Path) -> Self::Fut,
{
    type Fut: Future<Output = Out> + 'a;
}

impl<'a, F, Fut, Out> AsyncPathFn<'a, Out> for F
where
    F: Fn(&'a Path) -> Fut,
    Fut: Future<Output = Out>,
    Fut: 'a,
{
    type Fut = Fut;
}

However it doesn't work with closures.

// This now works.
async fn test_1() {
    load_using(load).await;
}

// This doesn't compile.
async fn test_2() {
    load_using(|path: &'_ Path| async { load(path).await }).await;
}

playground

3 Likes

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.