Feature suggestion: _async counterparts for std methods that accept closures

It'd be pretty nice for some of the stdlib methods that accept closures to have xyz_async counterparts, that accept asynchronous closures, allowing you to continue using .await just as you would in the parent scope (assuming the parent scope is an asynchronous function or closure).

Perhaps these would be best on separate traits, such as IteratorAsync<T> or AsyncIterator<T> as a counterpart for Iterator<T>.

Consider the following example:

macro_rules! rint_api {
  ($t:ident) => {
    &format!(r#"https://www.random.org/integers/?format=plain&col=1&base=10&min={min},&max={max}&num=1"#,
       min = std::$i::MIN,
       max = std::$t::MAX);
  },
  ($min:literal > $t:ident max > $max:literal) => {
     &format!(r#"https://www.random.org/integers/?format=plain&col=1&base=10&min={min},&max={max}&num=1"#,
       min = $min,
       max = $max);
  },
}

thread_local! {
  static FOO: RefCell<i32> = RefCell::new(0);
}

#[tokio::main]
async fn main() {
  // Since this is an asynchronous function, you can use .await here...
  
  FOO.with(|foo| {
    // ... but not here, since this is a synchronous closure.
    *foo += 42;
  });
  
  FOO.with_async(async move |foo| {
    // Here, you _could_ use .await, since it is now an asynchronous closure.
    // However, this method doesn't exist, hence why I'm making this post.
    let to_add = match reqwest::get(rint_api!(-512 > i32 > 512)).await {
      Ok(response) => match response.text().await() {
        Ok(text) => match text.parse::<i32>() {
          Ok(to_add) => to_add,
          Err(error) => panic!(format!("{}", error)),
        },
        Err(error) => panic!(format!("{}", error)),
      },
      Err(error) => panic!(format!("{}", error)),
    };
    *foo += to_add;
  }).await;

  FOO.with_async(async move |foo| {
    0..10.for_each_async(async move || {
      let to_add = match reqwest::get(rint_api!(-512 > i32 > 512)).await {
        Ok(response) => match response.text().await() {
          Ok(text) => match text.parse::<i32>() {
            Ok(to_add) => to_add,
            Err(error) => panic!(error),
          },
          Err(error) => panic!(error),
        },
        Err(error) => panic!(error),
      };
      *foo += to_add;
    }).await;
  }).await;
}

Its worth noting that a workaround is possible, but it's much less ergonomic: ( Playground link ( Note: Broken, doesn't compile because of a CC error. Works if compiled locally. ) )

Now I know that asynchronous closures are still unstable, but this would be an amazing feature to have. Let me know what you all think!

Imo this is the kind of thing that could be done in a library like itertools. This would allow details to be hammered out before it (possibly) gets merged into stdlib.

1 Like

That could work, although I think it would probably fit best in its own lib, as itertools and whatnot seem more targeted towards one specific part of std.

Async iterator sounds like Stream, depending on what you imagined there. (Look at StreamExt for its API.) It supports being created from an iterator and has API that can accept asynchronous closures (i.e. closures returning Futures), for example the asynchronous map equivalent StreamExt::then. Or an asynchronous StreamExt::for_each, basically taking async closures.

Regarding thread locals, a version of with that can accept an async closure seems a bit difficult, since to uphold the same guarantees that the created reference cannot leave the call to with and then leave then outlive the current thread you would, AFAICT, need to restrict the lifetime of the returned future in a way that makes it uncallable with proper async closures (since they want to store their arguments in the returned future.

I think the best solution to your code example would be to limit the scopes of calls to with to be way smaller, e.g. just doing FOO::with(|foo| *foo += to_add); calls.


So, here’s a playground with all these suggestions applied.

4 Likes

Oh sweet, I had no idea the Stream api existed. I've been using rust for a bit less than a year, but I'm new to async.