Accessing internal lifetimes in async code (`FnOnce` is not general enough)

I posted this on SO and didn't get any useful responses so I thought I would post it here as well, I hope I am not violating community rules. If I am, I appologise

I want to make a function that creates some internal state, lets the user modify it and then proceeds to do other stuff. This works fine in the normal case but I also want to give the user the opportunity to do async stuff in their computation. When the future constraint comes into play everything goes to hell:

#![feature(trait_alias)]

use core::future::ready;
use core::future::Future;
use core::sync::atomic::AtomicU8;

// A function that lets the user modify an inner value.
fn mod_ref(f: impl for<'a> FnOnce(&'a AtomicU8)) -> u8 {
    let a = AtomicU8::new(3);
    f(&a);
    a.into_inner()
}

// Same function but async.
trait AsyncW<'a, F> = FnOnce(&'a AtomicU8) -> F where F: Future<Output = ()>;
async fn mod_w_async<F>(f: impl for<'a> AsyncW<'a, F>) -> u8 {
    let a = AtomicU8::new(3);
    f(&a).await;
    a.into_inner()
}

fn main() {
    // This works fine.
    mod_ref(|_| ());

    // error: implementation of `FnOnce` is not general enough
    mod_w_async(|_| ready(()));
}

playgrount

error: implementation of `FnOnce` is not general enough
  --> src/main.rs:27:5
   |
27 |     mod_w_async(|_| ready(()));
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^ implementation of `FnOnce` is not general enough
   |
   = note: closure with signature `fn(&'2 AtomicU8) -> std::future::Ready<()>` must implement `FnOnce<(&'1 AtomicU8,)>`, for any lifetime `'1`...
   = note: ...but it actually implements `FnOnce<(&'2 AtomicU8,)>`, for some specific lifetime `'2`

Why is this happening and is there a way to get it to work?

Note that mod_ref and mod_w_async are cut down for brevity. The important part is that the argument function takes as an argument a reference to the function's stack (or rather future's state machine).

For what it's worth, it works fine if you get rid of the type alias, but I can't figure out a way to get it to work with the type alias.

async fn mod_w_async<F>(f: impl for<'a> FnOnce(&'a AtomicU8) -> F) -> u8
where
    F: Future<Output = ()>,

^ This works, but no amount of fiddling with the type alias has worked for me.

3 Likes

Heh, this could deserve being quoted: it's actually a real phenomenon in the Rust language; I might go into details explaining this later in the post.


In this instance, however, the first and foremost culprit seems indeed to be the trait alias. That is, if we go and focus on mod_ref (no async whatsoever!), but using a trait alias layer:

trait SyncW<'a> = FnOnce(&'a AtomicU8) -> ();
// A function that lets the user modify an inner value.
fn mod_ref(f: impl for<'a> SyncW<'a>) -> u8 {
    let a = AtomicU8::new(3);
    f(&a);
    a.into_inner()
}

we end up with the same error message about "not general enough".


Now, higher-order ("general enough") closures is indeed a subtle topic, and one where the code is very sensitive to what should have otherwise been semantically equivalent changes.

I was aware that the "stable Rust trait alias hack/polyfill" suffered from such limitation, but it is interesting (and disheartening) to see that even "proper" trait aliases suffer from it as well.

  • So your best solution here is to copy-paste the actual bound in your API.

  • FWIW, if the caller explicitly annotates the type of the input as &_ (e.g, |_: &_| or |x: &_|), then that error goes away.

  • but that's where your initial post would kick in: it's still very likely to cause problems with async for different but related issues.

See the following post for more info:

3 Likes