制御の逆転 〜 OchaCaml と限定継続でイベンドハンドラをダイレクトスタイルで書く

昔、こういう記事を書いた:

OchaCamlで限定継続/answer type modificationを勉強する - keigoiの日記

もっと shift/reset で何か身の回りの例を書いてみたい。そこで限定継続を使って、イベントハンドラをコールバックではなくダイレクトスタイルで書けるようにする、ということをやってみる。

ユースケース

例がとても古くて恐縮だが、たとえば古きよきショッピングカートの web アプリを作るときは、普通はページのルーティングを書いて、クリックの度に発行される GET か POST リクエストに反応するイベントドリブンで構成することになる:

/* /item_list アイテム一覧ページ */
Page onItemList();

/* /confirm 注文の確認ページ */
Page onConfirm(Cart);

/* /thankyou 支払い完了ページ */
Page onThankyou()

これはよくあるパターンだけど、プログラムの制御の流れは「ぶつ切り」になり見辛いし、ページ遷移の流れもわからない…と言い張ることもできる。

しかし、もし限定継続があったならば、ページ遷移の流れをプログラムの制御フローにより直接的に書き下すことができる:

var cart = [];
while(true) {
  cart, checkout = show_item_list();
  if (checkout) {
    ok = show_confirm_page();
    if (ok) {
      break;
    }
  }
}
show_thankyou_page();

(実際、かつては継続を使った web アプリのフレームワークがいくつかあった。 Kahua とか。)

プログラム

int 型のイベント発生源があるとして、そのハンドラをコールバックではなくダイレクトスタイルで書いてみよう。
まずはライブラリコード:

(* int 型の穴をもつ継続 *)
type 'a t = Fini | Cont of (int / 'a -> 'a t / 'a)
let cont = ref Fini

(* 関数 body をイベントハンドラに登録する関数 *)
let register body = 
  cont := 
    reset (fun () -> 
      body (fun () -> shift (fun k -> Cont k))
    )

(* イベントハンドラ *)
let event x =
  match !cont with 
  | Fini -> failwith "finished" 
  | Cont k -> 
    cont := k x

実際にダイレクトスタイルでイベントハンドラを書いてみる:

(* int型のイベントを 2回待ってその和を表示する *)
register (fun f -> 
  let x = f () in 
  let y = f () in
  print_int (x+y); 
  Fini);;

イベント発生をシミュレートしてみましょう。 10, 20 が順番に起こったとする。

# event 10;;
- : unit = ()
# event 20;;
30- : unit = ()

見事、ダイレクトスタイルで書いたイベントハンドラが動き、和が表示されました。やったぜ。

これくらい Python の yield とかでも出来てしまうけれど、そのあたりはご愛嬌。
もっと専門的な例を見てみたい人は Continuation Workshop 2011 Tutorial を参照しましょう。
OchaCaml は研究用の言語ではあるものの、 Hindley-Milner 型推論と answer type modification があるので安全かつ自由度の高いプログラムが書けるのだ。