Closure returning LocalBoxFuture, still requires borrowed data to be `'static`

Heya, in the following code, I have:

  • An #[async_trait] Trait implemented on a Struct, that uses &Data.
  • A closure which captures a borrowed &Data, calling r#struct.trait_fn(data).await

And I don't understand why the &Data needs to be 'static.

The error message:

error: lifetime may not live long enough
  --> src/main.rs:41:13
   |
38 |   pub async fn call_try_fold(graph: &Graph, data: &Data) {
   |                                                   - let's call the lifetime of this reference `'1`
...
41 | /             async move {
42 | |                 let seed = seed + r#struct.trait_fn(data).await;
43 | |
44 | |                 Result::<_, ()>::Ok(seed)
45 | |             }
46 | |             .boxed_local()
   | |__________________________^ returning this value requires that `'1` must outlive `'static`

Code (playpen):

// async-trait = "0.1.73"
// futures = "0.3.28"

use futures::{future::LocalBoxFuture, stream, FutureExt, StreamExt, TryStreamExt};

pub struct Graph([Struct; 1]);
pub struct Struct;
pub struct Data(u8);

impl Graph {
    async fn graph_try_fold<E, Seed, FnFold>(&self, seed: Seed, fn_fold: FnFold)
    where
        FnFold: Fn(Seed, &Struct) -> LocalBoxFuture<'_, Result<Seed, E>>,
    {
        let fn_fold = &fn_fold;
        let _ = stream::iter(self.0.iter())
            .map(Result::<_, E>::Ok)
            .try_fold(seed, |seed, r#struct| async move {
                fn_fold(seed, r#struct).await
            })
            .await;
    }
}

#[async_trait::async_trait]
pub trait Trait {
    async fn trait_fn(&self, data: &Data) -> u16;
}

#[async_trait::async_trait]
impl Trait for Struct {
    async fn trait_fn(&self, data: &Data) -> u16 {
        data.0.into()
    }
}

#[allow(unused_variables)]
pub async fn call_try_fold(graph: &Graph, data: &Data) {
    graph
        .graph_try_fold(1u16, |seed, r#struct| {
            async move {
                let seed = seed + r#struct.trait_fn(data).await;

                Result::<_, ()>::Ok(seed)
            }
            .boxed_local()
        })
        .await;
}

fn main() {
    let _a = call_try_fold(&Graph([Struct]), &Data(0));
}

Commenting out the let seed = seed + r#struct.trait_fn(data).await; line makes it compile, but I couldn't figure out what was causing the 'static requirement, whether it is:

  • FnFold's bound not including enough lifetime restrictions (and rust defaulting to 'static)
  • Something inside async_trait (didn't seem to be the case, I used cargo-expand and it seemed fine).
The cargo-expand for the async trait impl.
impl Trait for Struct {
    fn trait_fn<'life0, 'life1, 'async_trait>(
        &'life0 self,
        data: &'life1 Data,
    ) -> ::core::pin::Pin<
        Box<
            dyn ::core::future::Future<
                Output = u16,
            > + ::core::marker::Send + 'async_trait,
        >,
    >
    where
        'life0: 'async_trait,
        'life1: 'async_trait,
        Self: 'async_trait,
    {
        Box::pin(async move {
            if let ::core::option::Option::Some(__ret) = ::core::option::Option::None::<
                u16,
            > {
                return __ret;
            }
            let __self = self;
            let __ret: u16 = { data.0.into() };
            #[allow(unreachable_code)] __ret
        })
    }
}

I guess there's two things I'm asking for:

  • Help to understand what's going on, and ways to resolve it.
  • Better diagnostics :smile:

:crab: Classic. Struggle for hours, post something, then solve it soon after.

So the following solves it, using the implicit lifetime technique. The technique:

Where the problem lied (laid?), change:

async fn graph_try_fold<E, Seed, FnFold>(&self, seed: Seed, fn_fold: FnFold)
where
    FnFold: Fn(Seed, &Struct) -> LocalBoxFuture<'_, Result<Seed, E>>,

to:

async fn graph_try_fold<'f, E, Seed, FnFold>(&'f self, seed: Seed, fn_fold: FnFold)
where
    for<'f_fold> FnFold:
        Fn(Seed, &'f_fold Struct, &'f &'f_fold [(); 0]) -> LocalBoxFuture<'f, Result<Seed, E>>,

which means adding an additional argument in the API consumer's closure:

graph.graph_try_fold(1u16, |seed, r#struct| { .. }
graph.graph_try_fold(1u16, |seed, r#struct, _| { .. }

The solved playpen.

Ideally the API consumer doesn't have the additional argument; the "Improving the ergonomics" part of that post is probably reading material.


Another iteration of the solution:

  • no additional param in caller's closure (nicer API)
  • stores limit lifetime in the Graph structure (less nice?)
  • no need to Box the future (yay, nicer API)
code
// futures = "0.3.28"

use std::{future::Future, marker::PhantomData, pin::Pin};

use futures::{stream, FutureExt, StreamExt, TryStreamExt};

pub struct Graph<'env>([Struct<'env>; 1]);
pub struct Struct<'env>(PhantomData<&'env ()>);
pub struct Data(u8);

impl<'env> Graph<'env> {
    async fn graph_try_fold<'f, E, Seed, FnFold, Fut>(&'f self, seed: Seed, fn_fold: FnFold)
    where
        FnFold: Fn(Seed, &'f Struct<'_>) -> Fut,
        Fut: Future<Output = Result<Seed, E>> + 'f,
    {
        let fn_fold = &fn_fold;
        let _ = stream::iter(self.0.iter())
            .map(Result::<_, E>::Ok)
            .try_fold(seed, |seed, r#struct| async move {
                fn_fold(seed, r#struct).await
            })
            .await;
    }
}

pub trait Trait {
    fn trait_fn<'f>(&'f self, data: &'f Data) -> Pin<Box<dyn Future<Output = u16> + 'f>>;
}

impl<'env> Trait for Struct<'env> {
    fn trait_fn<'f>(&'f self, data: &'f Data) -> Pin<Box<dyn Future<Output = u16> + 'f>> {
        async { data.0.into() }.boxed()
    }
}

#[allow(unused_variables)]
pub async fn call_try_fold(graph: &Graph<'_>, data: &Data) {
    graph
        .graph_try_fold(1u16, |seed, r#struct| async move {
            let seed = seed + r#struct.trait_fn(data).await;

            Result::<_, ()>::Ok(seed)
        })
        .await;
}

fn main() {
    let _a = call_try_fold(&Graph([Struct(PhantomData)]), &Data(0));
}
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.