Arantium Maestum

プログラミング、囲碁、読書の話題

OCaml 5.0のEffect Handlerのチュートリアルをやりはじめた

12月16日にOCaml 5.0が正式にリリースされた。

目玉機能としてはMulticoreサポートで、以前はPythonなどと同じくGlobal Interpreter Lockがあって複数スレッドでCPUバウンドな計算が同時に実行できなかったのを、ガベージコレクタをはじめとしたランタイムの大幅な更新により並行計算が可能となった。

それに関連してEffect Handlerという機能が入った。これは並行処理にも使えるが応用範囲は非常に幅広い機能で、まだ構文や型システムが決まりきっていないので実験的にライブラリとして使えるようになっている。モナドなどと似た、さまざまな副作用や制御フローを統一的に扱える機能で、正式採用されればOCamlプログラムの書き方が大きく変わる可能性もあり、私も含めた一部界隈ではMulticore以上に注目されている印象。

その新機能のチュートリアルがあるのでやりはじめた:

github.com

このチュートリアルOCamlのEffect Handlerの実装がまだ実験段階だった(masterブランチにマージされるかも怪しいくらいの)時期からあったものだが、最終的にマージされた実装に合わせて今年修正されたようで、OCaml 5.0で追えるようだ。

ちなみに以下のPRで元々考えられていた専用構文から現在のライブラリ・関数を使ったEffect Handler構文に変更されたようなので、今後Effectsが専用構文付きで入るとしたらどのような構文になるかもしれないかを知る上では面白いかもしれない:

github.com

このチュートリアルの流れとしては

  • 投げられたエラーを処理した上で、エラーが生じた点から続行する
  • 可変なステート
  • ジェネレータ
  • 無限ストリーム
  • コルーチン
  • Async/Await
  • ネットワークなどの非同期IO

といった機能をEffect Handlerで実装・解説するものとなっていて、かなり勉強になる。これから数回にわけてチュートリアルの内容や演習問題について書いていきたい。

とりあえず今回は第一の例である「投げられたエラーを処理した上で、エラーが生じた点から続行する」という例について見てみたい。以下の1.2 Basicsというセクションである:

github.com

ここのコードの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 typegeneralized algebraic data typesである。

type ... += ...となっているのがextensible variant typeの構文で、既存のバリアント型に新しいコンストラクタを追加する、という意味になる。最近のOCamlではExceptionもこの機構を使っていて、ユーザ定義した例外もexn型のバリアントの一つとして追加できるようになっている。Exceptionを魔拡張したものという観点からすると、Effectもその機構を利用しているのは自然な流れかもしれない。

type _ t = T : a -> b tという構文がGADTだ。この言語機構については以前記事を書いた:

zehnpaard.hatenablog.com

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 lperform関数に渡している。ちなみにperformEffectsモジュール自体に含まれる唯一の関数で、これ以外のEffect関連関数などはすべてEffects.DeepかEffects.Shallowのいずれかに属している。

Conversion_failure lint 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が普及していくにはやはり当初の計画通り専用構文で隠蔽していく必要があるのではないだろうか、というのが現在の印象である。

discuss.ocaml.org

上記のOCaml Discussでの発表の通りEffect関連の型システムの詳細が決まった時点で専用構文の導入を検討するということなので、現状では好事家が試験的に使ってみる段階ということなのだろう。