osiire’s blog

ふしぎなそふとやさん

多相バリアントを使いこなそう(2)

前回は、OCamlの多相バリアント型のタグは複数の多相バリアント型に属する事ができるというお話でした。では、この性質を応用するとどんな嬉しいことがあるのでしょうか?今日はまず、多相バリアントを組み合わせる場合のストーリーを挙げてみたいと思います。

多相バリアント型を組み合わせる

例えば、キーボードイベントとマウスイベントを定義するバリアント型があったとします。

# type keyboard_event = KeyPress of char | KeyRelease of char;;
type keyboard_event = KeyPress of char | KeyRelease of char
# type mouse_event = 
    MousePress of int * int 
  | MouseRelease of int * int 
  | Click of int * int;;
type mouse_event = 
    MousePress of int * int 
  | MouseRelease of int * int 
  | Click of int * int

そして、マウスイベントとキーボードイベントを両方包括するようなイベント型も作ります。

# type event = Key of keyboard_event | Mouse of mouse_event;;
type event = Key of keyboard_event | Mouse of mouse_event

はい、何の問題もないように思えます。ただなにやら気になるのは、event型を作るときに、新しくKeyやMouseというタグを追加した事です。よく考えると、これはすごく邪魔です。本当に作りたいのは、単にキーボードイベントとマウスイベントを並列に並べたバリアント型であって、余計な階層を増やしたものではないはずです。

# type event = keyboard_event | mouse_event 
(* 本当はこう書きたい. でも出来ない *)

そこで、多相バリアントの出番です。多相バリアント型のタグは特定の多相バリアント型に属していなくてもよいので、例えば`KeyPress of charというタグはkeyboard_event型にもevent型にも属せるはずです。という事は、うまく型定義してやれば...

# type keyboard_event = [ `KeyPress of char | `KeyRelease of char ];;
type keyboard_event = [ `KeyPress of char | `KeyRelease of char ]
# type mouse_event = 
  [ `MousePress of int * int
  | `MouseRelease of int * int
  | `Click of int * int ];;
type mouse_event = 
  [ `MousePress of int * int
  | `MouseRelease of int * int
  | `Click of int * int ]
# type event = [ keyboard_event | mouse_event ];; (* 組み合わせる *)
type event =
    [ `Click of int * int
    | `KeyPress of char
    | `KeyRelease of char
    | `MousePress of int * int
    | `MouseRelease of int * int ]

やった、見事に`KeyPressタグをkeyboard_event型にもevent型にも属させることができました。素晴らしい。

操作の再利用性

実はこのフラット化は、単に新しいタグを作るのが面倒とかいう話ではなく、多相バリアント型を扱う操作(関数)の再利用性に大きな力ももたらしてくれます(本当はこっちが目的だったりします)。例えば、普通のバリアント型を用いたkeyboard_event型用の関数として、char型の引数を取り出すget_char関数を作ったとします。

let get_char = function 
  KeyPress c -> c
| KeyRelease c -> c;;
val get_char : keyboard_event -> char = <fun>

この関数はkeyboard_event型でしか使えません。もしevent型からキーボードの押された文字を取得したい場合には、追加して次の関数を作る必要があります。

let get_char_from_event = function 
  Key (KeyPress c) -> c
| Key (KeyRelease c) -> c
| _ -> failwith "not a key";;
val get_char_from_event : event -> char = <fun>

Keyという余計な階層が増えているためです。ところが多相バリアント型を使うと、なんと一度keyboard_event型用にget_char関数を作っておけば、event型でも再利用ができます。

let get_char = function (* 再利用可能なget_char関数 *)
  `KeyPress c -> c
| `KeyRelease c -> c
| _ -> failwith "not a key";;
val get_char : [> `KeyPress of 'a | `KeyRelease of 'a ] -> 'a = <fun>

このget_char関数の型の意味は、「`KeyPress of 'aと`KeyRelease of 'aというタグを持つ多相バリアント型を受け取って、引数と同じ型'aの値を返す関数」となります。これなら引数がkeyboard_event型でもevent型でもget_char関数に適用できますね。

要点

ちょっと長くなりましたが、要するに、多相バリアント型は組み合わせて使っても、組み合わせる前のそれぞれのタグに対する操作をそのまま引き継げるので、プログラムの再利用性を高めることができて嬉しい、という訳です。
このテクニックの極意は次の論文に記述されています。ぜひ参照してみてください。
http://www.math.nagoya-u.ac.jp/~garrigue/papers/fose2000.html
次回は多相バリアント型を開いたり閉じたりしてみます。