I am trying to build a module allowing scoped access to some global state. It allows setting and getting the state as well as waiting for updates of said state.
I assumed that in the following program, the Get effect would be handled by my effect handler because of the nesting but alas it doesn’t happen.
I intuitively understand why - since I’m using shallow handlers the first time an eio effect is raised (on both
) Eio handles the effect and resumes the continuation in a context where my effect handler is no longer in scope so to speak.
Nonetheless it’s a bit surprising and I’m having a hard time understanding how I should’ve built this and how effect handlers compose. I would appreciate pointing out where my mental model (or lack thereof ) is incorrect.
module type State = sig
type t
val run_with_eio : (unit -> unit) -> init:t -> unit
val get : unit -> t
val update : (t -> t) -> unit
val wait_for_update : unit -> unit
end
module Make (S : sig
type t
end) : State with type t = S.t = struct
open Effect
open Effect.Shallow
type t = S.t
type _ Effect.t += Get : t Effect.t
type _ Effect.t += Update : (t -> t) -> unit Effect.t
type _ Effect.t += Wait_for_update : unit Effect.t
let get () =
print_endline "performing Get";
perform Get
let update f = perform (Update f)
let wait_for_update () = perform Wait_for_update
let run_with_eio =
let rec loop :
type a r.
t ->
(a, r) continuation ->
a ->
(unit Eio.Promise.t * unit Eio.Promise.u) ref ->
r =
fun state k x p ->
print_endline "resume continuation";
continue_with k x
{
retc =
(fun result ->
print_endline "returning";
result);
exnc = (fun e -> raise e);
effc =
(fun (type b) (eff : b Effect.t) ->
match eff with
| Get ->
print_endline "handling Get";
Some (fun (k : (b, r) continuation) -> loop state k state p)
| Update updater ->
Some
(fun (k : (b, r) continuation) ->
print_endline "handling Update";
let next_state = updater state in
let () =
if next_state != state then
let resolver = snd p.contents in
let () = Eio.Promise.resolve resolver () in
p := Eio.Promise.create ()
in
loop next_state k () p)
| Wait_for_update ->
print_endline "handling Wait_for_update";
Some
(fun (k : (b, r) continuation) ->
let () = Eio.Promise.await (p.contents |> fst) in
loop state k () p)
| eff ->
print_endline (Printexc.to_string (Effect.Unhandled eff));
None);
}
in
fun f ~init ->
let p = ref (Eio.Promise.create ()) in
loop init (fiber f) () p
end
let%expect_test "Cooperative scheduling" =
let module IntState = Make (struct
type t = int
end) in
Eio_main.run (fun _ ->
IntState.run_with_eio ~init:0 (fun () ->
Eio.Fiber.both
(fun () ->
print_endline "yielding";
Eio.Fiber.yield ();
IntState.update (fun x -> x + 1);
print_newline ();
print_endline "back to first";
print_int (IntState.get ()))
(fun () ->
print_newline ();
print_endline "trying to get new state";
print_int (IntState.get ());
IntState.wait_for_update ();
print_newline ();
print_int (IntState.get ()))));
[%expect ""]
Here’s the output of my program (never mind the Stdlib.Effect.Unhandled(
prefix on each line. I just quickly hacked printing effects using Printexc
).
+|[@@expect.uncaught_exn {|
+| (* CR expect_test_collector: This test expectation appears to contain a backtrace.
+| This is strongly discouraged as backtraces are fragile.
+| Please change this test to not include a backtrace. *)
+|
+| ("Stdlib.Effect.Unhandled(Traffic_controller.State.Make(S).Get)")
+| Raised at Traffic_controller__State.Make.run_with_eio.loop.(fun) in file "traffic-controller/lib/state.ml", line 44, characters 27-34
+| Called from Eio_luv.run.(fun) in file "lib_eio_luv/eio_luv.ml", line 1287, characters 18-29
+| Re-raised at Eio_luv.run in file "lib_eio_luv/eio_luv.ml", line 1298, characters 20-55
+| Called from Traffic_controller__State.(fun) in file "traffic-controller/lib/state.ml", line 83, characters 2-635
+| Called from Expect_test_collector.Make.Instance_io.exec in file "collector/expect_test_collector.ml", line 262, characters 12-19
+|
+| Trailing output
+| ---------------
+| resume continuation
+| Stdlib.Effect.Unhandled(Eio__core__Cancel.Get_context)
+| Stdlib.Effect.Unhandled(Eio__core__Cancel.Get_context)
+| Stdlib.Effect.Unhandled(Eio__core__Fiber.Fork(_, _))
+| yielding
+| Stdlib.Effect.Unhandled(Eio__core__Cancel.Get_context)
+| Stdlib.Effect.Unhandled(Eio__core__Fiber.Fork(_, _))
+|
+| trying to get new state
+| performing Get
+| Stdlib.Effect.Unhandled(Eio__core__Cancel.Get_context)
+| Stdlib.Effect.Unhandled(Eio__core__Suspend.Suspend(_)) |}]