Converting FnOnce to AsyncFnOnce

I have the following async fn, see Playground

async fn transactional_save<F>(
    db: DatabaseConnection,
    callback: F,
) -> Result<User, TransactionError<DbErr>>
where
    F: FnOnce(&User, &DatabaseTransaction) + Send + 'static,
{
    db.transaction::<_, User, DbErr>(|txn| {
        Box::pin(async move {
            let user = User;

            callback(&user, txn);

            Ok(user)
        })
    })
    .await
}

and I am trying to convert

F: FnOnce(&User, &DatabaseTransaction) + Send + 'static,

To

F: ASyncFnOnce(&User, &DatabaseTransaction) + Send + 'static,

So that callback(&user, txn); becomes callback(&user, txn).await?;

My Attempt

This is what I have tried, see Playground

async fn transactional_save<F>(
    db: DatabaseConnection,
    callback: F,
) -> Result<User, TransactionError<DbErr>>
where
    F: AsyncFnOnce(&User, &DatabaseTransaction) + Send + 'static,
{
    db.transaction::<_, User, DbErr>(|txn| {
        Box::pin(async move {
            let user = User;

            callback(&user, txn).await;

            Ok(user)
        })
    })
    .await
}

error: future cannot be sent between threads safely
  --> src/lib.rs:45:9
   |
45 | /         Box::pin(async move {
46 | |             let user = User;
47 | |
48 | |             callback(&user, txn).await;
49 | |
50 | |             Ok(user)
51 | |         })
   | |__________^ future created by async block is not `Send`
   |
   = help: within `{async block@src/lib.rs:45:18: 45:28}`, the trait `Send` is not implemented for `<F as AsyncFnOnce<(&User, &db::DatabaseTransaction)>>::CallOnceFuture`
note: future is not `Send` as it awaits another future which is not `Send`
  --> src/lib.rs:48:13
   |
48 |             callback(&user, txn).await;
   |             ^^^^^^^^^^^^^^^^^^^^ await occurs here on type `<F as AsyncFnOnce<(&User, &db::DatabaseTransaction)>>::CallOnceFuture`, which is not `Send`
   = note: required for the cast from `Pin<Box<{async block@src/lib.rs:45:18: 45:28}>>` to `Pin<Box<dyn Future<Output = Result<User, db::DbErr>> + Send>>`

What does the compiler try to tell me? I cannot comprehend the meaning. I thought I have marked F to be Send ?
If I change the signature to the following then it compiles.

 //  F: AsyncFnOnce(&User, &DatabaseTransaction) + Send + 'static,
    F: FnOnce(&User, &DatabaseTransaction) -> Fut + Send + 'static,
    Fut: Future<Output = ()> + Send

F returns a Future<Output = ()>, and that return type is what does not have a Send bound. And a downside of the AsyncFn* traits is that they do not stably have a way to enforce that bound (you can't name the future type). On unstable the fix looks like so:

async fn transactional_save<F>(
    db: DatabaseConnection,
    callback: F,
) -> Result<User, TransactionError<DbErr>>
where
    F: AsyncFnOnce(&User, &DatabaseTransaction) + Send + 'static,
    // the new part:
    for<'a, 'b> <F as AsyncFnOnce<
        (&'a User, &'b db::DatabaseTransaction)
    >>::CallOnceFuture: Send,

There are some common workarounds that usually boil down to returning a

Pin<Box<dyn 'lifetime + Future<Output = ()> + Send>>

or so... like sea_orm is already doing.

where
    F: for<'a> FnOnce(&'a User, &'a DatabaseTransaction) 
        -> Pin<Box<dyn 'a + Future<Output = ()> + Send>> + Send + 'static,

Good Lord. I hope they're trying to improve that somewhat? It's not the worst, but I wouldn't want to do that more than a couple of times...

4 Likes

Maybe we'll get RTN on parameters directly, or if not you could probably mess around and find a way to make it apply anyway.[1]

Buuuuut I don't know how much better that is, and I also think it depends on how you look at it. That bound I wrote is a standard HRTB; the only non-vanilla thing about it is that using AsyncFnOnce as a normal trait is unstable -- even though it operates as a normal trait as far as things like associated types and bounds go.

So it may not be pretty, but is it really better to add layers and layers of special casing and magic? IMO it'd be better to stabilize using closure traits as traits. The existing functionality can handle this use case, after all.

(Or if we do add more simpler-looking[2] syntactic sugar, it should be generally applicable, vs. too narrowly focused.)


  1. I didn't try for this example ↩︎

  2. generally when Rust does this, it is actually more complicated in reality ↩︎

1 Like

There's a couple of surprising parts, one that the lifetime annotations requiring the HRTB are necessary there when they can be elided directly above and they are not mentioned in the return type, the next that you need to qualify the trait in use for F at all, given it only has the one bound so it doesn't seem like there's ambiguity.

In short, it seems like seemingly existing or at least very similar Rust features should let you write only

where
    F: AsyncFnOnce(&User, &DatabaseTransaction) + Send + 'static,
    // the new part:
    F::CallOnceFuture: Send,

Or with fn traits as regular generics, just:

where
    F: AsyncFnOnce<(&User, &DatabaseTransaction), CallOnceFuture: Send> + Send + 'static,

(I think that's roughly the syntax?)

Still fiddly, but the kind of fiddly that's already common when writing these things.

1 Like

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.