4.2 多相バリアント

4.2.1. 多相バリアントの弱点

1.4 節「レコードとバリアント 」で紹介したバリアントは、データ構造やアルゴリズムを組み立てるときにとても便利です。しかしモジュール化プログラミングで使っていると、時として柔軟性に欠けることがあります。これは、コンストラクタが個々に名前を予約して、個別の型に対応させることに起因します。異なる型に同じ名前を使うことはできません。ある型の値が、複数の構成子のある別の型に属すということを考えてみてください。

多相バリアントを使えば、この根本的な前提を取り除きます。つまり、バリアントタグは特にどの型にも属しません。型システムはバリアントタグの使われ方から、そのバリアントタグが許される値かどうかを判断するだけです。バリアントタグは使用する前に型を宣言する必要がありません。バリアント型はその使われ方それぞれから独立に推論されます。

基本的な使い方

プログラム中では、多相バリアントは通常のバリアントと同じように使えます。ただし、名前の前にバッククオート(`)をつける必要があります。

#[`On; `Off];;
- : [> `Off | `On] list = [`On; `Off]

#`Number 1;;
- : [> `Number of int] = `Number 1

#let f = function `On -> 1 | `Off -> 0 | `Number n -> n;;
val f : [< `Number of int | `Off | `On] -> int = <fun>

#List.map f [`On; `Off];;
- : int list = [1; 0]
      

[>`Off|`On] list は、すくなくとも `Off`On にマッチするもののリストを意味します(`Off`On ともに引数なし)。 [<`On|`Off|`Number of int] は、 f`Off`On(ともに引数なし)、もしくは `Number nnint)を受け取ることを意味します。バリアントの型の中の >< は、これらの型がタグを増やしたり減らしたりといった変更が可能なことを示しています。そのようなものは、暗黙の型変数を含んでいます。どちらのバリアント型も型中で一度しか現れないので、暗黙の型変数は表示されません。

上記のバリアント型は多相的で、変更が可能です。型注釈を書く場合には固定バリアント型、すなわち変更できない型として書かれることがほとんどでしょう。これは型の略記形[3]でも同様です。そのような型は >< を含まず、通常のデータ型の記述と同様に単にタグと関連した型を並べてあるだけです。

#type 'a vlist = [`Nil | `Cons of 'a * 'a vlist];;
type 'a vlist = [ `Cons of 'a * 'a vlist | `Nil]

#let rec map f : 'a vlist -> 'b vlist = function
   | `Nil -> `Nil
   | `Cons(a, l) -> `Cons(f a, map f l)
 ;;
val map : ('a -> 'b) -> 'a vlist -> 'b vlist = <fun>
      

高度な使い方

多相バリアントの型チェックは微妙なもので、型情報が通常よりも複雑になってしまう式も存在します。

#let f = function `A -> `C | `B -> `D | x -> x;;
val f : ([> `A | `B | `C | `D] as 'a) -> 'a = <fun>

#f `E;;
- : _[> `A | `B | `C | `D | `E] = `E
      

ここではふたつのことが起こっています。 まずこのパターンマッチは開いているので(最後の場合ですべてのタグを捕まえています)、型は、閉じた [< `A | `B] ではなく [> `A | `B] になります。 それから、 x がそのまま返るので、入力の型と戻り値の型は同一です。 'a という表記はその共有を表しているのです。 f をまた別のタグ `E に適用すると、それがまたリストに追加されます。

#let f1 = function `A x -> x = 1 | `B -> true | `C -> false
 let f2 = function `A x -> x = "a" | `B -> true ;;
val f1 : [< `A of int | `B | `C] -> bool = <fun>
val f2 : [< `A of string | `B] -> bool = <fun>

#let f x = f1 x && f2 x;;
val f : [< `A of string & int | `B] -> bool = <fun>
      

ここでは f1f2 はどちらもバリアントタグ `A`B を受け取りますが、 `A の引数は f1 では intf2 では string です。 f の型には、 f1 だけが受け取る `C は現れず、 `A の引数型には int & string と両方が現れています。 これはすなわち、バリアントタグ `Af に渡す場合には、 `A の引数は intstring の両方でなければならないということです。 このような値は存在しないため、 f`A には適用できず、 `B だけが入力として与えられるということになります。

ある値の型が固定的なバリアントであっても、型変換によって、それをより大きな型にすることもできます。 型変換は通常、変換元の型と変換先の型の両方を指定しなければなりませんが、単純な場合には元の型を省略することができます。

#type 'a wlist = [`Nil | `Cons of 'a * 'a wlist | `Snoc of 'a wlist * 'a];;
type 'a wlist = [ `Cons of 'a * 'a wlist | `Nil | `Snoc of 'a wlist * 'a]

#let wlist_of_vlist  l = (l : 'a vlist :> 'a wlist);;
val wlist_of_vlist : 'a vlist -> 'a wlist = <fun>

#let open_vlist l = (l : 'a vlist :> [> 'a vlist]);;
val open_vlist : 'a vlist -> [> 'a vlist] = <fun>

#fun x -> (x :> [`A|`B|`C]);;
- : [< `A | `B | `C] -> [ `A | `B | `C] = <fun>
      

パターンマッチを使って値を選択的に型変換することもできます。

#let split_cases = function
   | `Nil | `Cons _ as x -> `A x
   | `Snoc _ as x -> `B x
 ;;
val split_cases :
  [< `Cons of 'a | `Nil | `Snoc of 'b] ->
  [> `A of [> `Cons of 'a | `Nil] | `B of [> `Snoc of 'b]] = <fun>
      

バリアントタグを組み合わせた or パターンが alias パターンの中に現れた場合には、その alias には or パターンで列挙されたタグだけを含む型がつけられます。 これによって、関数の漸次定義などの便利なイディオムが実現できます。

#let num x = `Num x
 let eval1 eval (`Num x) = x
 let rec eval x = eval1 eval x ;;
val num : 'a -> [> `Num of 'a] = <fun>
val eval1 : 'a -> [ `Num of 'b] -> 'b = <fun>
val eval : [ `Num of 'a] -> 'a = <fun>

#let plus x y = `Plus(x,y)
 let eval2 eval = function
   | `Plus(x,y) -> eval x + eval y
   | `Num _ as x -> eval1 eval x
 let rec eval x = eval2 eval x ;;
val plus : 'a -> 'b -> [> `Plus of 'a * 'b] = <fun>
val eval2 : ('a -> int) -> [< `Num of int | `Plus of 'a * 'a] -> int = <fun>
val eval : ([< `Num of int | `Plus of 'a * 'a] as 'a) -> int = <fun>
      

これをもっと便利に使うために、型定義を or パターンの略記として使うこともできます。 すなわち、 type myvariant = [`Tag1 int | `Tag2 bool] を定義すると、 #myvariant というパターンは (`Tag1(_ : int) | `Tag2(_ : bool)) と書いたのと等価になります。

このような略記を単独で使うこともできますし、

#let f = function
   | #myvariant -> "myvariant"
   | `Tag3 -> "Tag3";;
val f : [< `Tag1 of int | `Tag2 of bool | `Tag3] -> string = <fun>
      

alias と組み合わせることもできます。

#let g1 = function `Tag1 _ -> "Tag1" | `Tag2 _ -> "Tag2";;
val g1 : [< `Tag1 of 'a | `Tag2 of 'b] -> string = <fun>

#let g = function
   | #myvariant as x -> g1 x
   | `Tag3 -> "Tag3";;
val g : [< `Tag1 of int | `Tag2 of bool | `Tag3] -> string = <fun>
      

4.2.1  多相バリアントの弱点

多相バリアントの威力を見ると、なぜこれで通常のバリアントを置き換えずに、新たに追加することにしたのか疑問を覚える人もいるでしょう。

答えはふたつあります。 ひとつには、多相バリアントは、非常に効率的である一方で、最適化のための静的型情報が欠落しているため、通常のバリアントよりもわずかに低速だということがあります。しかしながら、この違いが顕著になるのは巨大なデータ構造を扱った場合だけです。

より重要なのは、多相バリアントは型安全であるとはいうものの、それはより弱い型制約においてのことだということです。 すなわち、通常のバリアントは実際に型安全性を保証するだけでなく、宣言された型構成子だけを使い、データ構造中に存在する構成子はすべて互換性があり、構成子のパラメータにに型制約が付いていることを検査しています。

このことから、多相バリアントを使っている場合には型を明示するようより深く注意しなければなりません。 ライブラリを書いている場合には、インタフェースに精確な型を書くことができるので簡単です。 しかし、単純なプログラムには通常のバリアントを使っておいた方がよいでしょう。

特定のイディオムによって簡単なエラーが見つけにくくなることにも注意してください。 例えば、次のコードは恐らく間違っていますが、コンパイラにはそれを検出する術がありません。

#type abc = [`A | `B | `C] ;;
type abc = [ `A | `B | `C]

#let f = function
   | `As -> "A"
   | #abc -> "other" ;;
val f : [< `A | `As | `B | `C] -> string = <fun>

#let f : abc -> string = f ;;
val f : abc -> string = <fun>
      

このような危険性は定義自体に注釈を付けることで避けることができます。

#let f : abc -> string = function
   | `As -> "A"
   | #abc -> "other" ;;
Warning: this match case is unused.
val f : abc -> string = <fun>
      


[3] 訳注: type t = `On | `Off