Effect Handlers (OCaml 5)

Effect handlers generalize exceptions: a handler can resume the computation that performed the effect.

Effect handlers generalize exceptions: a handler can resume the computation that performed the effect. They power lightweight concurrency (Eio), generators, async/await, dependency injection, and more.

35.1 Declaring and performing effects

open Effect
open Effect.Deep

type _ Effect.t += Ask : string -> int Effect.t      (* effect returning int *)
type _ Effect.t += Log : string -> unit Effect.t

let program () =
  perform (Log "starting");
  let x = perform (Ask "x") in
  let y = perform (Ask "y") in
  perform (Log (Printf.sprintf "sum = %d" (x + y)));
  x + y

35.2 Handling effects (OCaml 5.3+ syntax)

let run () =
  match program () with
  | v -> Printf.printf "Result: %d\n" v
  | effect (Ask name), k -> continue k (String.length name * 10)
  | effect (Log msg), k -> print_endline ("[log] " ^ msg); continue k ()

35.3 Handling effects (OCaml 5.0–5.2 record syntax)

Also try_with (no return-value handler) and Effect.Shallow for shallow handlers.

let run () =
  match_with program ()
    { retc = (fun v -> Printf.printf "Result: %d\n" v);
      exnc = raise;
      effc = (fun (type a) (eff : a Effect.t) ->
        match eff with
        | Ask name -> Some (fun (k : (a, _) continuation) -> continue k (String.length name * 10))
        | Log msg -> Some (fun k -> print_endline ("[log] " ^ msg); continue k ())
        | _ -> None) }

35.4 Continuations

continue k v — resume with value v.

discontinue k exn — resume by raising exn at the perform site.

Continuations are one-shot: resuming twice raises Continuation_already_resumed.

Unhandled effects raise Effect.Unhandled.

35.5 Example: generators from iterators

let to_seq (type a) (iter : (a -> unit) -> unit) : a Seq.t =
  let module M = struct type _ Effect.t += Yield : a -> unit Effect.t end in
  let rec step () =
    match iter (fun x -> perform (M.Yield x)) with
    | () -> Seq.Nil
    | effect (M.Yield x), k -> Seq.Cons (x, fun () -> continue k ())
  in
  fun () -> step ()

let s = to_seq (fun f -> List.iter f [1; 2; 3])
(* Note: a one-shot Seq — consume it once. *)

35.6 Example: cooperative scheduler

type _ Effect.t += Yield : unit Effect.t | Fork : (unit -> unit) -> unit Effect.t

let run main =
  let q = Queue.create () in
  let next () = if not (Queue.is_empty q) then (Queue.pop q) () in
  let rec spawn f =
    match f () with
    | () -> next ()
    | effect Yield, k -> Queue.push (fun () -> continue k ()) q; next ()
    | effect (Fork g), k -> Queue.push (fun () -> continue k ()) q; spawn g
  in
  spawn main

let () = run (fun () ->
  perform (Fork (fun () -> for i = 1 to 3 do Printf.printf "A%d " i; perform Yield done));
  for i = 1 to 3 do Printf.printf "B%d " i; perform Yield done)
(* interleaved output *)

35.7 Effects vs monads

Effects provide direct-style code (no let* everywhere, no function coloring). The type system does not (yet) track which effects a function performs — that is a current limitation; unhandled effects are runtime errors.