While eio looks very promising, there is a massive showstopper for us (Routine) which is that, in my perception, it is hardly interoperable with other effects. There has been a discussion were I have explained my vision of this, and another one dealing it seems with the same issue. I have not found any other relevant discussion on this topic since that. We’d like to switch to eio in the near future, but this completely blocks adoption for us. Since the conversation dried up in the first topic, I’d like to open this dedicated new one to address the issue (or absence thereof) directly. I will restate my perceived issue with this, and would like to :
- Be proven wrong wrt the fact I need this, in which case I can happily proceed.
- Fail to convince that this is indeed a problem, and have to assess other solutions (another scheduler, or yet another homemade one).
- Be told that this is going to be addressed and to please stay seated until it’s done.
- Convince that this is a big misfeature, and work together towards a solution.
The issue in a nutshell.
This does not work (tested with eio 1.1) :
(** A dummy effect for demonstration purpose *)
type _ Stdlib.Effect.t += X : unit Stdlib.Effect.t
(** Run [f] while handling our dummy effect *)
let handle_x f =
Stdlib.Effect.Deep.try_with f ()
{
effc =
(fun (type effect) (effect : effect Effect.t) ->
match effect with
| X ->
Some
(fun (k : (effect, _) Effect.Deep.continuation) ->
Effect.Deep.continue k ())
| _ -> None);
}
let () =
Eio_main.run @@ fun _ ->
(* Handle our dummy effect from inside eio's run *)
handle_x @@ fun () ->
Eio.Fiber.both (fun () -> Stdlib.Effect.perform X) (fun () -> ())
Our effect is not handled like, I think, one could expect:
Fatal error: exception Stdlib.Effect.Unhandled(Dune__exe__Foo.X)
Funnily enough if you swap the arguments to Eio.Fiber.both
, it accidentally works, which does not really honor the principle of least suprise. This is because eio sensibly runs the second callback directly instead of forking twice.
If you handle_x
above Eio_main.run
, this works as expected.
Why I argue this is a very problematic misfeature
- I fundamentally believe in the composability of features. That is, if an ecosystem has features to solve a problem, these features must all compose to address composite problems and to be able to combine two different libraries that use an arbitrary set of those features.
- I believe effects are a great flow control feature that must be freely usable. They certainly must not be overused, but in some case I think they are the cleanest most maintainable solution, especially to avoid side effects. I know this opinion is not shared by everyone, and that some would go as far as to say that effects are basically just there to implement eio-like scheduler and that’s it, but no matter where you fall on that spectrum I don’t think it’s the place of a library (eio in this instance) to take that decision for you and take effects away from you.
In our case at hand :
- Eio is the way to perform asynchronous I/O in direct style.
- Effects are (IMO) the correct way to perform many control flow operation, such as providing a global context to a call tree, a pure implementation of mutable state without devolving in a nightmarish sub-optimal state monad proliferation, etc. I would happily discuss why I think so, but I’m trying to keep this already loaded conversation clean of it since, as I explained, I don’t think it’s up to eio to decide this for me.
- All our software stack is based on asynchronous I/O and such effects control flow, so both must compose.
Why it currently doesn’t work
Last time as checked, when forking a fiber, eio sends a Fork
effect up to the scheduler (Eio_main.run
) which will store in its fiber it for later continuation. By doing so the Fork
effects gets out of our effect handler, and the callback it holds will be run and made into a fiber outside of its context, thus explaining the lack of our custom effect handler.
How this could work
Rather than plagiarizing myself, I will point you to my previous implementation, which both reproduces the problem described above and fixes it by creating and continuing the fibers at the fork point, so below the custom effect handler.
It requires to alter the scheduling of eio quite a bit, since now fibers are organized as a tree, not just a flat list with routines loosely waiting on each other. It’s only a proof of concept, so there could be some issues ahead that I missed, and I’m willing to help addressing these.