Nesting Algebraic Handlers found in Different Modules

Can algebraic handlers in OCaml be separated and put into different modules, then be nested in some other file?

For instance, say there are two compilation units Handler1 and Handler2, each with their ml and mli files. They each contain some effect E and have a function that handles the effect, something like:

let handlerE prog () = match_with prog () { ... handler ... } (* in Handler1*)
let handlerF prog () = match_with prog () { ... handler ... } (* in Handler2*)

In another file, run_handlers.ml, can these two handlers be nested in some way? Will the effects in prog go unhandled if we do so, contrary to what would happen if the handlers all in the same file?

Yes, effects and handlers can be defined independently in different modules. In fact, splitting the definitions across two module should not change anything.

For instance, the toy example below work as expected by nesting the choice handler under the rand handler:

module D = Effect.Deep

module Rand = struct
   type _ Effect.t +=
     | Bool: bool Effect.t
     | Int : int -> int Effect.t
  let bool () = Effect.perform Bool
  let int bound = Effect.perform (Int bound)
  let handle f x=
    let effc (type a r) (e:a Effect.t): ((a,r) D.continuation -> r) option =
      match e with
      | Int n -> Some (fun k -> D.continue k @@ Random.int n)
      | Bool  -> Some (fun k -> D.continue k @@ Random.bool ())
      | _ -> None
    in
    Effect.Deep.match_with f x { retc = Fun.id; exnc = raise; effc }

  let mock f x=
    let effc (type a r) (e:a Effect.t): ((a,r) D.continuation -> r) option =
      match e with
      | Int n -> Some (fun k -> D.continue k 0)
      | Bool  -> Some (fun k -> D.continue k true)
      | _ -> None
    in
    Effect.Deep.match_with f x { retc = Fun.id; exnc = raise; effc }

end

module Choice = struct
  type _ Effect.t +=
   | Int : int * int -> int Effect.t
  let int x y = Effect.perform (Int(x,y))
  let handle f x =
    let effc (type a r) (e:a Effect.t): ((a,r) D.continuation -> r) option =
      match e with
      | Int (x,y) ->
        Some (fun k ->
            if Rand.bool ()
            then D.continue k x
            else D.continue k y
          )
      | _ -> None
    in
    Effect.Deep.match_with f x { retc = Fun.id; exnc = raise; effc }
end

let test () = Rand.int 5 + Choice.int 1 2

let ()  =
  Format.printf "@[<v>mock result=%d,@ result=%d@]@."
    (Rand.mock (Choice.handle test) ())
    (Rand.handle (Choice.handle test) ())
1 Like

For instance, say you have a program in a file called program.ml that uses a user-defined function put: int -> unit that modifies a variable x. In this scenario, there is an effect Put_found: int -> unit Effect.t and put is defined as

put n = x:= n; perform(Put_found n)

A sample program would look like: put(5);put(0).

There are also two compilation units Handler1 and Handler2, each with their ml and mli files. The former contains a function run_h1 that when given the sample program as input, the handler acts on the effect Put_found by printing the integer passed to it. Then, it raises another effect Report_p, which is to be passed to the second handler. run_h1 returns a function of type unit -> unit as looks like this:

let run_h1 () = fun () -> match_with Program.comp () {
effc = (fun (type c) (eff: c Effect.t) ->
    match eff with
    | Put_found s -> Some (fun (k : (c,_) continuation) ->
            report_p(s); continue k ())
    | _ -> None
  );
}

The second effect, Report_p of type int -> unit Effect.t, is to be handled by a function found in Handler2 such that if the integer passed is negative, an error message is printed to the screen.

run_h2 is the following function:

let run_h2 inst x = match_with inst x
{
    effc = (fun (type b) (eff: b Effect.t) ->
      match eff with
      | Report n -> Some (fun (k: (b,_) continuation) -> 
        if n = (-1) then printf "Put with value -1 encountered.\n" else printf "Put found with allowed value.\n"; continue k ())
      | _ -> None
    );
    exnc = raise;
    retc = fun x-> x
}

In another file, run_handlers.ml, I make use of the compilation units mentioned above as follows:

let run_handlers () = Handler2.run_h2(Handler1.run_h1 ()) ()

When at first the program and effects were found in the same file and the handlers were nested in the same file, the program worked fine. The program looked like this:

let run_h2 () = 
  let comp () = put(5); put(6); put(-1); put(100) in
  let run_h1 () = match_with comp ()
  { effc = (fun (type c) (eff: c Effect.t) ->
      match eff with
      | Put_found s -> Some (fun (k: (c,_) continuation) ->
              report_p(s); continue k ())
      | _ -> None
    );
    exnc = raise;
    retc = fun x-> x
  } in
  match_with run_h1 () {
    effc = (fun (type b) (eff: b Effect.t) ->
      match eff with
      | Report n -> Some (fun (k: (b,_) continuation) -> 
        if n = (-1) then printf "Put with value -1 encountered.\n" else printf "Put found with value: %d\n" (n); continue k ())
      | _ -> None
    );
    exnc = raise;
    retc = fun x-> x
  }

Now, when trying to modularise it as explained above, a Stdlib.Effect.Unhandled exception was thrown on the first instance the effect should be performed.

There is your error, Handler1.run_h1 () is evaluated outside of the run_h2 handler function, you meant

let run_handlers () = Handler2.run_h2 Handler1.run_h1 ()

I understand what you’re saying and it makes sense, so thank you very much for that.

When I tried out this alteration in my code, it seems that I have to change some types since now run_h2 is given Handler1.run_h1 as an argument. Is the type of this unit -> unit or is it unit -> (unit -> unit)? I am struggling a bit to understand what the return type of run_h1 is in this case. I am trying to follow the static semantics explanation given in Retrofitting Effect Handlers onto OCaml.

Hm, you are right; I missed the fact that you had a superfluous () in the definition of h1.

In this case, your code example works without any troubles.

The issue is elsewhere (and has nothing to do with modules).

It would help to see all of the code.

My only guess is the effect constructors aren’t shared (doing Effect.t += Report in module A and another Effect.t += Report in module B will result in distinct constructors A.Report and B.Report, you want to have just one Effect.t += Report in a shared module).

1 Like

It seems that since the effects were declared in separate files, they were indeed different constructors. Once I put the effects in one module and shared that amongst Handler1 and Handler2, then the problem was fixed. Thanks!

1 Like