jkb0o / pecs

Asynchronous operations for Bevy Engine
Apache License 2.0
64 stars 7 forks source link

async/await support #3

Open dlight opened 1 year ago

dlight commented 1 year ago

Hi, I just saw this project and I have a question, could pecs's promises implement Future so that it can be used with async/await? Not a feature request, just curious.

jkb0o commented 1 year ago

I've been asking myself the same question all last week. And the short answer is: NO, it isn't possible for pecs to implement Future.

There is a little bit longer answer:

pecs is all about (bevy) ecs at first point: it is important to be able to fetch Bevy's system params at any promise call (send events/query entites/modify resources, etc). This doesn't fit Future at all.

But.

But. What can be done is to expand the functionality of asyn! macro so it will parse the function body and expand it into multiple .then calls. It is not the same as implementing the Future, but it could look pretty close to classic async functions while keep the same perfect interop with Bevy's ecs (this approach is my personal goal for this project).

I'll try to demonstrate what I mean by code. Let's try to write asyn function that makes http request and logs the error or response status code and time spent using that super-cool-extended async! macro:

commands.add(asyn!(time: Res<Time> => {
  let started = time.elapsed_time();
  // when macro find this await it will split the body into promise chain
  let resp = await asyn::http::get("https://bevyengine.org");
  let delta = time.elapsed_time() - started;
  match resp {
    Ok(r) => info!("Bevy respond with {} in {delta:0.2}s", r.status),
    Err(e) => info!("`Got error in {delta:02}s: {e}");
  }
}))

This asyn! function looks like common async function, but it can access all Bevy's ecs features. In fact, the content of macro has any required data to expand itself into (already implemented) chain of promises:

commands.add(
  Promise::start(asyn!(_, time: Res<Time> => {
   asyn::http::get("https://bevyengine.org")
     .with(ime.elapsed_time())
  }))
  .then(asyn!(state, resp, time: Res<Time> => {
    let started = state.value;
    let delta = time.elapsed_time() - started;
    match resp {
      Ok(r) => info!("Bevy respond with {} in {delta:0.2}s", r.status),
      Err(e) => info!("`Got error in {delta:02}s: {e}");
    }
    Promise::resolve(())
  }))
)
jkb0o commented 1 year ago

I rethought my answer. There are two parts, actually:

It is possible to implement both of them. But turning promises into future requires to register it within the world first, so there is only possible api I see:

// it is possible to turn any Promise or AsynFunct into future
let future = commands.promise(asyn!(...)).future()

The second part looks much more usable:

async fn my_asyn_func() -> String {
  "".to_string()
}

fn setup(mut commands: Commands) {
  commands.add(
    Promise::start(asyn!(_ => {
      // promise may return Future
      my_asyn_func()
    })
    .then(asyn!(_, result => {
      info!("Custom future result: {result}");
      Promise::pass()
    })
  )
}