What trait bound does an async function statisfy?

Hi guys, I want to make a tonic rpc method retriable. A common rpc method has mutable refrence of client, for example:

async fn hello(client: &mut Client, request: Request) -> Response {
	/// do something
}

Further, I need to store "hello" function in a wrapper, then I can use it multiple times. for example

pub struct RpcRetryWrapper<C, I, O, F> {
	f: F,
	marker: std::marker::PhantomData<fn(&mut C, I) -> O>,
}

impl<C, I, O, F> RpcRetryWrapper
where
	F: // trait bound
{
	async fn call_once(&mut self, client: &mut C, request: I) -> O {
		// do something
	}

	async fn call_twice(&mut self, client: &mut C, request: I) -> O 
	where 
		I: Clone
	{
		let first_res = self.call_once(client, request.clone());
		let second_res = self.call_once(client, request);
		// do something 
	}
}

However, there is a question that I can not find correct trait bound of F. I guess the trait bound is something like this, but compiler will report an error.

F: for<'a> Fn(&'a mut C, I) -> impl Future<Output = O> + 'a

So, what trait bound can describe a async function that capcuture reference of a variable?

Just treat O as the future type and use <O as Future>::Output to get the final output of the async function.This may not fix all of your problems, but it does fix the immediate errors in your example

use std::future::Future;

pub struct RpcRetryWrapper<C, I, O, F> {
    f: F,
    marker: std::marker::PhantomData<fn(&mut C, I) -> O>,
}

impl<C, I, O, F> RpcRetryWrapper<C, I, O, F>
where
    F: for<'a> Fn(&'a mut C, I) -> O,
    O: Future,
{
    async fn call_once(&mut self, client: &mut C, request: I) -> O::Output {
        // do something
        todo!()
    }

    async fn call_twice(&mut self, client: &mut C, request: I) -> O::Output
    where
        I: Clone,
    {
        let first_res = self.call_once(client, request.clone()).await; // await necessary to avoid a borrow checker error
        let second_res = self.call_once(client, request);
        // do something
        todo!()
    }
}
1 Like

Thanks, I modify codes following your suggestions.
but compiler will report error when i try to use call_once().

use std::future::Future;

pub struct RpcRetryWrapper<C, I, F> {
	pub f: F,
	marker: std::marker::PhantomData<fn(&mut C, I)>,
}

  

impl<C, I, F> RpcRetryWrapper<C, I, F> {
	fn new(f: F) -> Self {
		Self {
			f,
			marker: std::marker::PhantomData,	
		}
	}
}

  

impl<C, I, O, F> RpcRetryWrapper<C, I, F>
where
	I: Clone,
	F: for<'a> Fn(&'a mut C, I) -> O,
	O: Future,
{
	async fn call_once(&self, client: &mut C, request: I) -> O::Output {
		(self.f)(client, request).await
	}
	async fn call_twice(&self, client: &mut C, request: I) -> O::Output {
		let first_res = (self.f)(client, request.clone()).await;
		let second_res = (self.f)(client, request.clone()).await;
		second_res
	}
}

#[cfg(test)]
mod tests {
	use crate::{call_exmaple, RpcRetryWrapper};

	#[tokio::test]
	async fn it_works() {
		let mut client = "client".to_string();
		let request = "hello".to_string();
		let wrapper: RpcRetryWrapper<String, String, _> = RpcRetryWrapper::new(call_exmaple);
		wrapper.call_once(&mut client, request); // error reported here.
	}
}

impl<C, I, O, F> RpcRetryWrapper<C, I, F>
where
	I: Clone,
	F: for<'a> Fn(&'a mut C, I) -> O,
	O: Future,

async fn outputs capture their input lifetimes, and so they cannot be represented by a type parameter like O when there is an input lifetime, as O must resolve to a single concrete type. The returned Futures are not all the same single type; they vary by lifetime.

There are verbose ways to try and get around that restriction in some cases, but last I knew they don't work for the async fn case (the compiler can't yet see through the indirection and too many higher-ranked bounds).

3 Likes

Here is my another try to solve this problem.

  1. construct a concrete type LifetimeBoundFutureWrapper for output future of async fn which capture input lifetimes. it seems ok to convert output of call_example into a LifetimeBoundFutureWrapper<'_, String, _>.
trait LifetimeBoundFuture<'a, T>: 'a + Future<Output = T> {}

impl<'a, T, F> LifetimeBoundFuture<'a, T> for F where F: 'a + Future<Output = T> {}

struct LifetimeBoundFutureWrapper<'a, T, F> {
    fut: F,
    marker: std::marker::PhantomData<fn() -> &'a T>,
}

impl<'a, T, F> From<F> for LifetimeBoundFutureWrapper<'a, T, F>
where
    F: LifetimeBoundFuture<'a, T>,
{
    fn from(fut: F) -> Self {
        Self {
            fut,
            marker: std::marker::PhantomData,
        }
    }
}

async fn call_exmaple(client: &mut String, request: String) -> String {
	request
}

#[cfg(test)]
mod tests {
    use crate::{call_exmaple, LifetimeBoundFutureWrapper, RpcRetryWrapper};

    #[tokio::test]
    async fn future_wrapper_test() {
        let mut client = "client".to_string();
        let request = "hello".to_string();

        let fut_wrapper: LifetimeBoundFutureWrapper<'_, String, _> =
            call_exmaple(&mut client, request).into();
    }
}
  1. Modify trait bound in RpcRetryWrapper
impl<C, I, O, F> RpcRetryWrapper<C, I, F>
where
    I: Clone,
    F: for<'a> Fn(&'a mut C, I) -> LifetimeBoundFutureWrapper<'a, O::Output, O>,
    O: Future,
  1. Finally, how to prove that call_example satisfy the trait bound. All i knew is to define an immediate trait like AsyncFuncExt to do blanket implementation for all async fn of this form.