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.

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.