How to compose effect handlers with Eio

Hmm I think things are quite subtle here. One problem is I think you’re hitting the same thing I was talking about in Escaping Effects which happens with Deep handlers too. I removed the printing of the unhandled effect because I found that a bit misleading (all of the Eio unhandled ones are just not handled by the inner state handler, but are handled by the Eio handler that’s wrapping it). Which just leaves:

resume continuation
yielding

trying to get new state
performing Get
Fatal error: exception Stdlib.Effect.Unhandled(Dune__exe__Main.Make(S).Get)

And this boils down to Eio’s implementation for Fiber.fork which is implicitly called by your Fiber.both. When you fork a function fn:

fn runs immediately, without switching to any other fiber first.

Which means fn is called in Eio’s handler and so you are outside of the scope of IntState handler.

If you simplify the whole thing down a lot more to just:

Eio_main.run (fun _ ->
    IntState.run_with_eio ~init:0 (fun () ->
          Eio.Fiber.both
            (fun () -> print_int (IntState.get ()))
            (fun () -> ())));

You get the exception. If you flip the handlers then you get a working program… but not for long, because if you introduce your Wait_for_update effect you implicitly raise an Eio effect by awaiting the promise and now your inverted handler program raises an exception!

IntState.run_with_eio ~init:0 (fun () ->
    Eio_main.run (fun _ ->
          Eio.Fiber.both
            (fun () -> IntState.wait_for_update ())
            (fun () -> ())));

handling Wait_for_update
Fatal error: exception Stdlib.Effect.Unhandled(Eio__core__Suspend.Suspend(_))

Also see this discussion from a while back Am I wrong about Effects? I see them as a step back - #15 by patricoferris – my conclusion there was composing handlers is tricky so I aim to be somewhat sparing in their use. However, I think some people are looking at how to define generic effects that people could reuse to build things like synchronisation primitives which might help composing that kind of thing, see this talk for more info on that: https://watch.ocaml.org/videos/watch/08ea09a1-e645-47cb-80c4-499dd4d93ac8 (note that’s schedulers and not generic handlers).

However, as I mentioned it’s even subtler than that when you bring in shallow handlers (the above problems are also there for deep handlers). Take this example with inverted handlers (compared to your original):

IntState.run_with_eio ~init:0 (fun () ->
    Eio_main.run (fun env ->
      Eio.Time.sleep env#clock 1.;
      print_int (IntState.get ())
  ))

If we remove the sleep, we get a normal working program. With the sleep (which on all Eio backends performs an effect) we get an unhandled Get again. And actually we can drill down a little deeper and discover it’s actually any Eio function call that performs the Suspend effect!

IntState.run_with_eio ~init:0 (fun () ->
    Eio_main.run (fun env ->
      Effect.perform (Eio.Private.Effects.Suspend (fun _ e -> e (Ok ())));
      print_int (IntState.get ())
  ))

And to be honest I don’t quite follow why this breaks but I think it’s because the fn of the Suspend is called inside the Eio handler which presumably (as you said) is where the shallow handler is no longer in scope… but that’s a bit hand wavy to me ^^" So in retrospect I think that this is the unhandled effect problem you are hitting, but it wouldn’t have been long before you hit the other ones I was talking about. Presumably an effect system would have just not compiled your program and left you to understand why :))

1 Like