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!" が表示される *)
注釈
-
With_exn.tの!は injectivity の指定のために必要らしいです。type _ Effect.t += Foo : ('a -> unit) -> 'a With_exn.t Effect.tのようなエフェクトを定義したいときに必要です。 ↩