blog.anqou.net
rss
author
tags

OCaml のエフェクトハンドラ内で起こる例外を呼び出し元に返却する

OCaml 5.0 から導入されたエフェクトハンドラを用いると、従来は難しかったような柔軟な処理を実現できます。例えば以下のような Foo というエフェクトを定義すると:

type _ Effect.t += Foo : int Effect.t

以下のように使えば 1, 2, 3 の順で処理が行われます:

let e =
  try Effect.perform Foo (* 1 *) + 10 (* 3 *)
  with effect Foo, k ->
    Effect.Deep.continue k 42 (* 2 *)
;;

assert (e = 52)

エフェクトハンドラは便利なのですが、従来では考える必要がなかったようなコードフローを考慮すべきときがあります。 Fun.protect を使い、例外が発生した場合でもクリーンナップをしてから終了するようなコードを考えてみます:

type _ Effect.t += Foo : unit Effect.t

let () =
  try
    Fun.protect ~finally:(
      fun () -> print_endline "cleanup!"
    ) @@ fun () ->
    Effect.perform Foo
  with effect Foo, k ->
    Effect.Deep.continue k ()

(* 実行すると "cleanup!" と表示される *)

しかしエフェクトハンドラで例外が発生してしまうと、このクリーンナップはスキップされてしまいます:

type _ Effect.t += Foo : unit Effect.t

let () =
  try
    Fun.protect ~finally:(
      fun () -> print_endline "cleanup!"
    ) @@ fun () ->
    Effect.perform Foo
  with effect Foo, k ->
    (* エフェクトハンドラが例外を投げる *)
    Effect.Deep.continue k
      (failwith "unexpected exception!")

(* "cleanup!" が表示されない! *)

これを解決するためには、エフェクトハンドラ内で例外をキャッチし、呼び出し元に戻したうえで再スローする必要があります。愚直に書いてもいいのですが、以下のようなモジュールを作ると便利です[1]

module With_exn : sig
  type !'a t

  val catch : (unit -> 'a) -> 'a t
  val raise : 'a t -> 'a
end = struct
  type 'a t = ('a, [ `Raised of exn ]) result

  let catch f = try Ok (f ()) with e -> Error (`Raised e)
  let raise x = match x with Ok x -> x | Error (`Raised e) -> raise e
end

type _ Effect.t += Foo : unit With_exn.t Effect.t

let () =
  try
    Fun.protect ~finally:(
      fun () -> print_endline "cleanup!"
    ) @@ fun () ->
    Effect.perform Foo |> With_exn.raise
  with effect Foo, k ->
    Effect.Deep.continue k @@
    With_exn.catch @@ fun () ->
    failwith "unexpected exception!"

(* "cleanup!" が表示される *)

注釈

  1. With_exn.t! は injectivity の指定のために必要らしいです。type _ Effect.t += Foo : ('a -> unit) -> 'a With_exn.t Effect.t のようなエフェクトを定義したいときに必要です。