ocaml-multicore / eio

Effects-based direct-style IO for multicore OCaml
Other
548 stars 66 forks source link

Return promise resolved in switch without waiting for switch to return #570

Closed hummy123 closed 1 year ago

hummy123 commented 1 year ago

Hi there.

Thanks to all the contributors for the valuable work they did on this library.

I have the following short function, and I'm hoping I could return the promise without waiting for Switch to finish. Then I could check for myself if a promise is resolved or not and use the returned value if so.

let create_promise model callback =
  let promise, resolver = Promise.create () in
  Switch.run (fun sw ->
    Fiber.fork ~sw (fun () -> 
      let next = callback model sw in
      Promise.resolve resolver next);
  );
  promise

Does EIO support something like this or similar? The readme says "the function can't return until Switch.run does" and the authors must have a good reason for this design.

I will try to explain below my use case and what I'm trying to do so (if not supported) I can hopefully receive advice and adapt to what the library authors have chosen.

-

I'm trying to create an Elm-like GUI framework (implemented as Immediate Mode/IMGUI) and I would like to add concurrency capabilities to it using EIO.

It supports two callbacks: a sync one with the signature (fun model -> model) and an async one with (fun model switch -> (fun model -> model)).

The reason an async callback is required to return a (fun model -> model) instead of just a model is because the end-user of applications built with the framework might click a button that executes a sync callback changing the model while waiting for an async callback to finish.

If that happens, the model returned by that async callback would be stale and outdated. This could be avoided if the result of an async callback returns a function describing how to change a model instead of the model itself.

The code I have right now has a list of promises (each promise created with the create_promise function at the top of this post) and checks if each promise in the list is resolved on each render. If it is, then the inner sync callback returned by the promise is applied to the model and the promise is removed from the list.

I hope that makes sense. I think what I am trying to do is reasonable but I'm happy to accept advice if anyone has a better way to go about it.

Thanks in advance, Humza

Edit: I think what's also possible is for me to have a root switch that runs the UI/render loop, and to also pass this switch to async functions.

It would mean I can remove Switch.run from the create_promise function and don't have to wait for the async function to finish.

The only problem I would have with this approach would be resource management, since the readme states that all resources are automatically freed when the switch is done running.

I don't see any guidance on how one could clean up resources manually, but I appreciate that this project is quite young and not at version 1.0.0 yet.

talex5 commented 1 year ago

If you want the new fiber to out-live the function, the function needs to take the switch as an argument. The caller providing the switch is then responsible for e.g. cancelling it when it's no longer needed. e.g.

let create_promise ~sw model callback =

I don't know enough about Elm to know whether you should use a single top-level switch, or whether you can make it more fine-grained.

Note that there is a Fiber.fork_promise, which would simplify your code a bit.

hummy123 commented 1 year ago

@talex5 Thanks for your advice! I'm a happy user of this library now and appreciate your help.