Async fn signature desugaring again

In the past I've written wrappers around backoff::retry to cleanly encapsulate retry behavior, such as with reqwest:

pub fn get<T: DeserializeOwned>(url: &str, token: &str) -> Result<T, String> {
    let op = || {
        Client::new()
            .get(url)
            .headers(headers(token))
            .send()
            .map_err(backoff::Error::transient)
    };

    call(op)
}

fn call<T, F, E>(op: F) -> Result<T, String>
where
    T: DeserializeOwned,
    F: FnMut() -> Result<Response, Error<E>>,
    E: Display,
{
    retry(backoff(), op)
        .map_err::<String, _>(|e| e.to_string())
        .and_then(|mut response| {
            let mut buf = String::new();
            response
                .read_to_string(&mut buf)
                .expect("HTTP response not valid UTF-8");
            if buf.contains("error") {
                Err(format!("Received Error Response: {}", buf))
            } else {
                serde_json::from_str(&buf)
                    .map_err(|e| format!("Could not parse response body: {:?}", e))
            }
        })
}

I wanted to do something similar with gRPC now.

But gRPC in Rust means Tonic which means async.

I can write this easily enough:

pub fn retry<T, F>(mut op: F) -> Result<T, backoff::Error<String>>
where
    F: FnMut() -> Result<T, String>,
{
    let op2 = || {
        block_in_place(|| Handle::current().block_on(async { op() }))
            .map_err(backoff::Error::transient)
    };
    backoff::retry(backoff::ExponentialBackoff::default(), op2)
}

But that's written for a non-async op. gRPC service calls are async.

Conceptually, I want to do something like

pub fn retry<T, F>(mut op: F) -> Result<T, backoff::Error<String>>
where
    F: **async** FnMut() -> Result<T, String>,
{
    let op2 = || {
        block_in_place(|| Handle::current().block_on(op()))
            .map_err(backoff::Error::transient)
    };
    backoff::retry(backoff::ExponentialBackoff::default(), op2)
}

But, of course, this is not possible - async doesn't work that way.

I'm pretty sure, based on what I've just read, that there's just no way to desugar async into some kind of trait bounds generic enough to use with any lambda - no way to define a function of async A -> B, etc. Because async fns get turned into state machines.

Is that right? Is this hopeless?

Is it literally a "async FnMut() -> ..", that is, a closure that takes no inputs?

If so, then the translation is

F: FnMut() -> Fut,
Fut: Future<Output = Result<Response, Error<E>>,

But if the future captures lifetime inputs, which is typical...

// Not actual Rust code
F: for<'a> FnMut(&'a SomeInput) -> 
       impl use<'a> + Future<Output = Result<Response, Error<E>>

...you probably need type erasure.

F: FnMut(&SomeInput) -> Pin<Box<dyn Future<..> + 'a [+ Send ..]>>
2 Likes

I typed it like that so that the caller would need to use closure syntax. I.e., a typical method call is actually

server.read_data(params)

but the caller would pass a closure that wraps that call.

For example, this is how I wrote a timing helper:

pub fn time<T>(label: &str, f: impl FnOnce() -> T) -> T {
    let now = Instant::now();
    let result = f();
    let elapsed = now.elapsed().as_millis();
    info!(
        "Time for {}: {}ms",
        label,
        elapsed.to_formatted_string(&Locale::en)
    );
    result
}

and it's called like this:

    time("thing.run()", || match thing.run(params) { ... }

Of course, sometimes you have to move for this to compile.

So your reply is encouraging! I think I may be able to do something analogous here after all.

This can be done in stable Rust (at least from perspective of defining the right lifetime requirements):

You probably meant AsyncFnMut1? AsyncFn0 is just the bound I suggested without a dependency (and Fn not FnMut).

See the comment I linked for a discussion of how it doesn't actually help due to current HRTB and closure inference/annotation limitations. (Short example.)

(Well, it can help sometimes, perhaps, but generally I see it fail.)

The cool thing is that this

fn example<F>(_: F)
where
    F: AsyncFn(&str) -> usize,
{
}

fn main() {
    example(async |s: &str| { s.len() })
}

already works on nightly. :slight_smile:

2 Likes

Sir, do not tempt me to the dark side (of nightly builds), please!!

(When will it be released?)

#62290 has been finished ~2 weeks ago and is already merged I hope it will be available soon. :slight_smile:

1 Like

Am I wrong or is this kind of game-changing?

For me at least this would reduce the pain of async a lot - although we sure need async traits...

I think it definitely improves the ergonomics a lot, but my last status was that there were still some rough edges - see here.

1 Like