How to create a function which accepts a function (callback)?

Hey, everyone. I'm starting to use Rust, and I'm having some difficulties with the compiler while trying to create a function which accepts another function as parameter. Currently, I'm doing this to avoid duplicating code in my Lambda function:

// main.rs
#[tokio::main]
async fn main() -> Result<(), Error> {
    tracing::init_default_subscriber();

    app(function_handler).await
}
// lambda.rs
use aws_sdk_dynamodb::Client;
use lambda_http::{Body, Error, Request, Response, run, service_fn, tracing};

pub async fn app<F, Fut>(lambda_handler: F) -> Result<(), Error>
where
    F: Fn(Request, &Client) -> Fut,
    Fut: Future<Output = Result<Response<Body>, Error>> + Send,
{
    tracing::init_default_subscriber();

    let config = aws_config::load_from_env().await;
    let dynamodb_client = Client::new(&config);
    
    run(service_fn(|event: Request| {
        lambda_handler(event, &dynamodb_client)
    }))
    .await
}

With this I get the compiling error: implementation of `Fn` is not general enough.

I've tried different approaches, but they always fail, what is the concept I'm failing to grasp here? Any tips?

Please provide a MRE. E.g. what does function_handler look like?

Sorry, my mistake, forgot to add it here:

pub async fn function_handler(event: Request, _client: &Client) -> Result<Response<Body>, Error> {
    let who = event
        .path_parameters_ref()
        .and_then(|params| params.first("name"))
        .unwrap_or("world");
    let message = format!("Hello, {who}!");

    let resp = Response::builder()
        .status(200)
        .header("content-type", "text/html")
        .body(message.into())
        .map_err(Box::new)?;
    Ok(resp)
}

Also, the crates being used are: lambda_http - Rust and aws_sdk_dynamodb - Rust.

I'm guessing it's not "general enough" with regards to the lifetime of your fn's reference. Try:

F: for<'c> Fn(Request, &'c Client) + use<'_> -> Fut,

I didn't work, unfortunately :(. However, the syntax doesn't seem quite right here.

this is a peculiar situation, where you hit the limitation of the sugared Fn trait syntax. I vaguely know the reason but cannot explain it clearly.

in this example, since the function_handler() doesn't need to capture the parameter (it is unused at all), you can workaround it by tweak the signature of function_handler a bit.

but I assume the client parameter is actually used in real code, so this workaround is more involed, namely, you can use a boxed future for the Fn bound which can specify higher ranked lifetimes, and you must manually box the future in the function_handler() function instead of relying on the async fn desugar.

pub async fn app<F>(lambda_handler: F) -> Result<(), Error>
where
	F: Fn(
		Request,
		&Client,
	) -> Pin<Box<dyn Future<Output = Result<Response<Body>, Error>> + Send + '_>>
{
    //...
}

pub fn function_handler(
	event: Request,
	_client: &Client,
) -> Pin<Box<dyn Future<Output = Result<Response<Body>, Error>> + Send + '_>> {
	Box::pin(async move {
		let who = event
			.path_parameters_ref()
			.and_then(|params| params.first("name"))
			.unwrap_or("world");
		let message = format!("Hello, {who}!");

		let resp = Response::builder()
			.status(200)
			.header("content-type", "text/html")
			.body(message.into())
			.map_err(Box::new)?;
			Ok(resp)
	})
}

I did something similar in one of the workarounds I was trying to find, but it didn't work. This one didn't as well:

// function_handler error
expected `Pin<Box<dyn Future<Output=Result<Response<Body>, Error>>+Send>>`, but found `Result<Response<_>, _>`

Also, the ? operator can only be used in async functions which return Result or Option.

I'll try to rebuild the solution I had before, which was similar to this and provide it here, so it might give some insight.

pub async fn app(
    lambda_handler: fn(
        event: Request,
        _client: &Client,
    ) -> Pin<Box<dyn Future<Output = Result<Response<Body>, Error>> + Send>>,
) -> Result<(), Error> {
    tracing::init_default_subscriber();

    let config = aws_config::load_from_env().await;
    let dynamodb_client = Client::new(&config);
    run(service_fn(|event: Request| {
        lambda_handler(event, &dynamodb_client)
    }))
    .await
}

However, this leads me to the error:

error[E0308]: mismatched types
  --> src/bin/hello-world-name/main.rs:24:9
   |
24 |     app(function_handler).await
   |     --- ^^^^^^^^^^^^^^^^ expected fn pointer, found fn item
   |     |
   |     arguments to this function are incorrect
   |
   = note: expected fn pointer `for<'a> fn(lambda_http::http::Request<_>, &'a Client) -> Pin<Box<(dyn Future<Output = Result<lambda_http::Response<lambda_http::Body>, Box<(dyn std::error::Error + Send + Sync + 'static)>>> + Send + 'static)>>`                                                                                                                                                                                                                      
                 found fn item `for<'a> fn(lambda_http::http::Request<_>, &'a Client) -> impl Future<Output = Result<lambda_http::Response<lambda_http::Body>, Box<(dyn std::error::Error + Send + Sync + 'static)>>> {function_handler}`                                                                                                                                                                                                                               
note: function defined here
  --> /home/lucas/Desktop/Repositories/Personal/best-blog-api/src/lambda.rs:5:14
   |
5  | pub async fn app(
   |              ^^^

this is how you can do it for async fn with the unstable unboxed_closures feature:

pub async fn app<F>(lambda_handler: F) -> Result<(), Error>
where
	F: for<'a> Fn<
			(Request, &'a Client),
			Output: Future<Output = Result<Response<Body>, Error>> + Send + 'a,
		> {
	//...
}

it may be possible to make it work on stable using a helper trait, but I'm not really into this kind of thing.

I honestly don't get why the already created service_fn function supports such behavior, but when coding it myself, it seems impossible.

In your function signature

pub async fn app<F, Fut>(lambda_handler: F) -> Result<(), Error>
where
    F: Fn(Request, &Client) -> Fut,
    Fut: Future<Output = Result<Response<Body>, Error>> + Send,

you have a generic parameter Fut, which is required to be the output of F. Because Fut is a separate generic parameter, its value cannot depend on what F does. But the F you want to use, function_handler, returns a future type which does depend on F — specifically, the future uses the borrow &Client and thus its type must contain the lifetime of that borrow.

The Fn bound on F is implicitly a HRTB — desugared it is for<'a> F: Fn(Request, &'a Client) -> Fut. The <F, Fut> pattern works only in cases where F's bound is not HRTB.

Instead, you must write your required bounds in some way that does not make use of a separate Fut parameter. The new AsyncFn trait does this, but has its own limitations; to work on stable you can use helper traits like those provided by async_fn_traits (or write your own for this specific case) which only specify the future as an associated type, which can therefore stay inside the scope of the implicit for<'a> in the bound on F.

5 Likes

I guess I have a lot to learn. Thanks!

1 Like