Tow HRTB (for<'c> ) :How to add an explicit lifetime bound

⚓ Rust    📅 2025-10-29    👤 surdeus    👁️ 2      

surdeus

I'm working on a database transaction proxy system and encountering lifetime issues when trying to create a wrapper around async callbacks. I have a TransactionTrait that defines a transaction method taking an async callback, and I'm trying to create a proxy that can wrap and enhance these callbacks.

I have two attempts at creating a proxy callback function, but both have lifetime issues:
Attempt 1: proxy_callback1

async fn proxy_callback1<F, T, E>(
    callback: F,
) -> impl for<'r> FnOnce(
    &'r DatabaseTransaction,
) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'r>>
+ Send
where
    F: for<'c> FnOnce(
            &'c DatabaseTransaction,
        ) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'c>>
        + Send,
    T: Send,
    E: Display + Debug + Send,
{
    move |dt: &DatabaseTransaction| {
        // ERROR: the parameter type `F` may not live long enough [E0310]
        // the parameter type `F` must be valid for the static lifetime..
        Box::pin(async move {
            do_something().await;
            callback(dt).await
        })
    }
}

Error: The compiler complains that type F may not live long enough and must be valid for the 'static lifetime.

Attempt 2: proxy_callback2

async fn proxy_callback2<'a, F, T, E>(
    callback: F,
) -> impl FnOnce(&'a DatabaseTransaction) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>
+ Send
+ 'a
where
    F: FnOnce(&'a DatabaseTransaction) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>
        + Send
        + 'a,
    T: Send,
    E: Display + Debug + Send,
{
    move |dt: &DatabaseTransaction| {
        // This compiles but is too restrictive
        Box::pin(async move {
            do_something().await;
            callback(dt).await
        })
    }
}

This version compiles but is too restrictive - it requires all lifetimes to be the same ('a ), which doesn't match the more flexible higher-ranked trait bounds (HRTB) used in the trait.

Attempt 3: Objective: Strengthen this closure

#[async_trait::async_trait]
impl TransactionTrait for DatabaseConnectionProxy {
    async fn transaction<F, T, E>(&self, callback: F) -> Result<T, TransactionError<E>>
    where
        F: for<'c> FnOnce(
                &'c DatabaseTransaction,
            ) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'c>>
            + Send,
        T: Send,
        E: Display + Debug + Send,
    {
        // Proxy   callback:

        let callback_proxy = move |dt: &DatabaseTransaction| {//dt lifetime:'1

            // lifetime may not live long enough
            // returning this value requires that `'1` must outlive `'2`
            // the parameter type `F` may not live long enough [E0310]
            // the parameter type `F` must be valid for the static lifetime..
            // todo: How to add an explicit lifetime bound
            Box::pin(async {
                // Do something :eg: log.info().await;
                callback(dt).await // callback lifetime :'2
            }) as Pin<Box<dyn Future<Output = Result<T, E>> + Send + '_>>
        };

        self.inner_conn.transaction(callback_proxy).await
    }
}

The Core Issue

I want to extract the proxy callback logic into a reusable function, but I can't seem to express the correct lifetime relationships. The trait uses HRTB (for<'c> ) to indicate that the callback should work for any lifetime 'c of the DatabaseTransaction , but when I try to create a standalone function, I either get lifetime errors or the signature becomes too restrictive.

What I've Tried

  1. Higher-ranked trait bounds in the return type
  2. Explicit lifetime parameters (but they become too restrictive)
  3. Various combinations of + 'static bounds

Questions

  1. Why does the inline closure work in the trait implementation but not when extracted to a function?
  2. How can I express the proper lifetime bounds for a standalone proxy callback function?
  3. Is there a way to maintain the flexibility of the HRTB while extracting this logic?

All Code

use std::fmt::{Debug, Display};
use std::pin::Pin;

pub enum TransactionError<E> {
    Connection,
    Transaction(E),
}
pub struct DatabaseTransaction;
#[async_trait::async_trait]
pub trait TransactionTrait {
    async fn transaction<F, T, E>(&self, callback: F) -> Result<T, TransactionError<E>>
    where
        F: for<'c> FnOnce(
                &'c DatabaseTransaction,
            ) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'c>>
            + Send,
        T: Send,
        E: Display + Debug + Send;
}
pub struct DatabaseConnection;
#[async_trait::async_trait]
impl TransactionTrait for DatabaseConnection {
    async fn transaction<F, T, E>(&self, callback: F) -> Result<T, TransactionError<E>>
    where
        F: for<'c> FnOnce(
                &'c DatabaseTransaction,
            ) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'c>>
            + Send,
        T: Send,
        E: Display + Debug + Send,
    {
        callback(&DatabaseTransaction).await.map_err(TransactionError::Transaction)
    }
}
pub struct DatabaseConnectionProxy {
    inner_conn: DatabaseConnection,
}
#[async_trait::async_trait]
impl TransactionTrait for DatabaseConnectionProxy {
    async fn transaction<F, T, E>(&self, callback: F) -> Result<T, TransactionError<E>>
    where
        F: for<'c> FnOnce(
                &'c DatabaseTransaction,
            ) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'c>>
            + Send,
        T: Send,
        E: Display + Debug + Send,
    {
        // Proxy   callback:

        let callback_proxy = move |dt: &DatabaseTransaction| {//dt lifetime:'1

            // lifetime may not live long enough
            // returning this value requires that `'1` must outlive `'2`
            // the parameter type `F` may not live long enough [E0310]
            // the parameter type `F` must be valid for the static lifetime..
            // todo: How to add an explicit lifetime bound
            Box::pin(async {
                // Do something :eg: log.info().await;
                callback(dt).await // callback lifetime :'2
            }) as Pin<Box<dyn Future<Output = Result<T, E>> + Send + '_>>
        };

        self.inner_conn.transaction(callback_proxy).await
    }
}

// simple function 1
async fn proxy_callback1<F, T, E>(
    callback: F,
) -> impl for<'r> FnOnce(
    &'r DatabaseTransaction,
) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'r>>
+ Send
where
    F: for<'c> FnOnce(
            &'c DatabaseTransaction,
        ) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'c>>
        + Send,
    T: Send,
    E: Display + Debug + Send,
{
    move |dt: &DatabaseTransaction| {
        // the parameter type `F` may not live long enough [E0310]
        // the parameter type `F` must be valid for the static lifetime..
        Box::pin(async move {
            do_something().await;
            callback(dt).await
        })
    }
}
// simple function 2
async fn proxy_callback2<'a, F, T, E>(
    callback: F,
) -> impl FnOnce(&'a DatabaseTransaction) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>
+ Send
+ 'a
where
    F: FnOnce(&'a DatabaseTransaction) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>
        + Send
        + 'a,
    T: Send,
    E: Display + Debug + Send,
{
    move |dt: &DatabaseTransaction| {
        // pass , but not general enough type :
        Box::pin(async move {
            do_something().await;
            callback(dt).await
        })
    }
}

async fn do_something() {
    println!("do_something");
}
fn main() {}

1 post - 1 participant

Read full topic

🏷️ Rust_feed