OCaml 5.0のEffect Handlerのチュートリアルをやりはじめた
12月16日にOCaml 5.0が正式にリリースされた。
目玉機能としてはMulticoreサポートで、以前はPythonなどと同じくGlobal Interpreter Lockがあって複数スレッドでCPUバウンドな計算が同時に実行できなかったのを、ガベージコレクタをはじめとしたランタイムの大幅な更新により並行計算が可能となった。
それに関連してEffect Handlerという機能が入った。これは並行処理にも使えるが応用範囲は非常に幅広い機能で、まだ構文や型システムが決まりきっていないので実験的にライブラリとして使えるようになっている。モナドなどと似た、さまざまな副作用や制御フローを統一的に扱える機能で、正式採用されればOCamlプログラムの書き方が大きく変わる可能性もあり、私も含めた一部界隈ではMulticore以上に注目されている印象。
その新機能のチュートリアルがあるのでやりはじめた:
このチュートリアルはOCamlのEffect Handlerの実装がまだ実験段階だった(masterブランチにマージされるかも怪しいくらいの)時期からあったものだが、最終的にマージされた実装に合わせて今年修正されたようで、OCaml 5.0で追えるようだ。
ちなみに以下のPRで元々考えられていた専用構文から現在のライブラリ・関数を使ったEffect Handler構文に変更されたようなので、今後Effectsが専用構文付きで入るとしたらどのような構文になるかもしれないかを知る上では面白いかもしれない:
このチュートリアルの流れとしては
- 投げられたエラーを処理した上で、エラーが生じた点から続行する
- 可変なステート
- ジェネレータ
- 無限ストリーム
- コルーチン
- Async/Await
- ネットワークなどの非同期IO
といった機能をEffect Handlerで実装・解説するものとなっていて、かなり勉強になる。これから数回にわけてチュートリアルの内容や演習問題について書いていきたい。
とりあえず今回は第一の例である「投げられたエラーを処理した上で、エラーが生じた点から続行する」という例について見てみたい。以下の1.2 Basicsというセクションである:
ここのコードのeffect関連の部分を漁っていく。
まずEffectモジュールの扱い:
open Effect open Effect.Deep
いきなりEffectとEffect.Deepをopenしている。OCaml作法として個人的にはモジュール冒頭でのopenはあまり好ましくないように思うのだが・・・ チュートリアルだからショートカットとしてこのようにしているのだろうか?チュートリアルとしても、この二つのどちらから特定の名前が入ってきているのかわからなくなってマイナスに感じる。
とにかく今回はEffect.Deepを使っている。別にEffect.Shallowというものもあり、両者の違いはチュートリアルの先の方で説明されているので詳しくはまた今度の記事に書く。
次に新しいEffectの定義:
type _ Effect.t += Conversion_failure : string -> int Effect.t
この1行でOCamlの公式レファレンスでいうところのLanguage Extension(言ってしまえば上級者機能)が二つ使われている。extensible variant typeとgeneralized algebraic data typesである。
type ... += ...
となっているのがextensible variant typeの構文で、既存のバリアント型に新しいコンストラクタを追加する、という意味になる。最近のOCamlではExceptionもこの機構を使っていて、ユーザ定義した例外もexn型のバリアントの一つとして追加できるようになっている。Exceptionを魔拡張したものという観点からすると、Effectもその機構を利用しているのは自然な流れかもしれない。
type _ t = T : a -> b t
という構文がGADTだ。この言語機構については以前記事を書いた:
Conversion_failure : string -> int Effect.t
となっているのでConversion_failure "some text that fails to convert"
という値はint Effect.t
型を持つ。
その定義されたEffectの使いどころとしては:
let int_of_string l = try int_of_string l with | Failure _ -> perform (Conversion_failure l)
と前述の通りint Effect.t
型となるConversion_failure l
をperform
関数に渡している。ちなみにperform
はEffectsモジュール自体に含まれる唯一の関数で、これ以外のEffect関連関数などはすべてEffects.DeepかEffects.Shallowのいずれかに属している。
Conversion_failure l
がint Effect.t
型であるので、perform (Conversion_failure l)
はint
型となる。つまりEffectを実行した結果として何らかのintが帰ってくるということだ。一般的にa Effect.t
をperformするとa型の値が返ってくる。
しかしこの例でint_of_string
がシャドーされてるのはどうなんだろう・・・。これもわかりにくい気がするが・・・。
とにかくこの例で定義されるint_of_string
から生じるConversion_failureというEffectをどのように扱うかを決めるEffect Handlerの定義:
let _ = ... match_with sum_up r { effc = (fun (type c) (eff: c Effect.t) -> match eff with | Conversion_failure s -> Some (fun (k: (c,_) continuation) -> Printf.fprintf stderr "Conversion failure \"%s\"\n%!" s; continue k 0) | _ -> None ); exnc = ...; retc = ... }
sum_up
というのがint_of_string
を内部で使っている関数で、その関数に渡したい引数がr
だ。
Effect.Deepで定義されているmatch_with
は実行したい関数とそれに渡す引数、そしてhandler型の三つを引数としてとる。
handlerはsum_up r
の実行中に
- effectを実行した場合の処理effc
- 例外を投げた場合のexnc
- 普通に関数適用が終了し値が返ってきた場合のretc
の三種類の関数からなるレコードである。
effcの定義に注目してみる:
fun (type c) (eff: c Effect.t) -> match eff with | Conversion_failure s -> Some (fun (k: (c,_) continuation) -> Printf.fprintf stderr "Conversion failure \"%s\"\n%!" s; continue k 0) | _ -> None
まずfun (type c) ... -> ...
というのはまたしてもOCaml Language ExtensionであるLocally Abstract Typeだ。このfunの引数であるeffと、この関数内で出てくる限定継続kの間に共有される型cが出てくる、という制約をつけるために使われている。
具体的にはeffの型はc Effect.t
なので前述の通りこのEffectが実行されるとc型が返ってくることを期待されている。そして継続kの型は(c,_) continuation
である。(a,b) continuation
はa型をとりb型を返す限定継続なので、(c,_) continuation
は「何を返すかはわからないけどとにかくc型を受け取る限定継続」となる。
最終的にcontinue k 0
している(continueはEffect.Deepで定義されている関数)のでperform (Conversion_failure l)
の結果は必ず0となる。
というわけでしっかり追っていくとなんとかBasicな例は理解できた。しかしExtensible Variant Types、GADT、Locally Abstract Typeを明示的に使い、そして限定継続を表す('a, 'b) continuation
型を明示的に型注釈する必要があるなど、OCamlの上級者向け機能が剥き出しでバンバン出てきて決して気軽に読み書きできるようなものではない。
Effect Handlerが普及していくにはやはり当初の計画通り専用構文で隠蔽していく必要があるのではないだろうか、というのが現在の印象である。
上記のOCaml Discussでの発表の通りEffect関連の型システムの詳細が決まった時点で専用構文の導入を検討するということなので、現状では好事家が試験的に使ってみる段階ということなのだろう。