パラメータ付きクラスも内容については多相的ですが、メソッドの使用に関して多相性を認めるには不十分です。
典型的な例として、反復子を定義します。
#
List.fold_left;;
- : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a = <fun>
#
class ['a] intlist (l : int list) = object method empty = (l = []) method fold f (accu : 'a) = List.fold_left f accu l end;;
class ['a] intlist : int list -> object method empty : bool method fold : ('a -> int -> 'a) -> 'a -> 'a end
少し見たところ、この多相的な反復子が定義できたように見えます。しかし、実際はそうではありません。
#
let l = new intlist [1; 2; 3];;
val l : '_a intlist = <obj>
#
l#fold (fun x y -> x+y) 0;;
- : int = 6
#
l;;
- : int intlist = <obj>
#
l#fold (fun s x -> s ^ string_of_int x ^ " ") "";;
This expression has type int but is here used with type string
上記の反復子の処理を見ると、最初は正常に動作しています。しかし、オブジェクト自体は多相的ではないため(多相的なのは構成子だけです)一度 fold
メソッドが使うと個々のオブジェクトの fold
の型が固定されてしまい、次に文字列の反復子として使おうとすると失敗するのです。
このような問題が生じるのは、量化子を書く場所が間違っているためです。クラスを多相的にしたいのではなく、 fold
メソッドを多相的にしたいのです。これは、メソッド定義で多相型を明示することにより可能になります。
#
class intlist (l : int list) = object method empty = (l = []) method fold : 'a. ('a -> int -> 'a) -> 'a -> 'a = fun f accu -> List.fold_left f accu l end;;
class intlist : int list -> object method empty : bool method fold : ('a -> int -> 'a) -> 'a -> 'a end
#
let l = new intlist [1; 2; 3];;
val l : intlist = <obj>
#
l#fold (fun x y -> x+y) 0;;
- : int = 6
#
l#fold (fun s x -> s ^ string_of_int x ^ " ") "";;
- : string = "1 2 3 "
ここでコンパイラの表示したクラス型を見て分かるように、クラス定義では多相メソッド型を明示しなければならないのに対し、クラス型では量化された型変数[1]は暗黙のままになっています。
なぜ型を明示して定義する必要があるのでしょうか。問題は (int -> int -> int) -> int -> int
も fold
の型としては正しいのですが、それが突然われわれの与えた多相型に適合しなくなってしまうのです(型変数の自動インスタンス化はトップレベルの型変数に対してのみ働き、それ以外の量化子には働きません。内側の量化子の自動インスタンス化は決定不能問題です)。
そのためコンパイラはどちらの型も選ぶことができず助けを求めてくるのです。
ただし、もし継承や自分自身への型制約によってメソッドの型が分っている時には、型を完全に省略することができます。メソッドを上書きする例を示します。
#
class intlist_rev l = object inherit intlist l method fold f accu = List.fold_left f accu (List.rev l) end;;
次の表記法では、記述と定義を分離しています。
#
class type ['a] iterator = object method fold : ('b -> 'a -> 'b) -> 'b -> 'b end;;
#
class intlist l = object (self : int #iterator) method empty = (l = []) method fold f accu = List.fold_left f accu l end;;
(self : int #iterator)
という表記に注意して下さい。これはこのオブジェクトが iterator
クラス型のインタフェースを実装していることを保障します。
多相メソッドは通常のメソッドと同じように呼び出すことができますが、型推論には制限があります。特に、多相メソッドは呼び出される地点で型が分っていなくてはいけないという制限です。そうでないと、メソッドは単相型を持つと仮定され、型が適合しなくなります。
#
let sum lst = lst#fold (fun x y -> x+y) 0;;
val sum : < fold : (int -> int -> int) -> int -> 'a; .. > -> 'a = <fun>
#
sum l;;
This expression has type intlist but is here used with type < fold : (int -> int -> int) -> int -> 'a; .. > Types for method fold are incompatible
この問題は簡単に解決できます。引数の型に制約を課せばよいのです。
#
let sum (lst : _ #iterator) = lst#fold (fun x y -> x+y) 0;;
val sum : int #iterator -> int = <fun>
もちろん、制約はメソッドの型を明示することでも指定できます。ただし型変数が量化されている必要があります。
#
let sum lst = (lst : < fold : 'a. ('a -> _ -> 'a) -> 'a -> 'a; .. >)#fold (+) 0;;
val sum : < fold : 'a. ('a -> int -> 'a) -> 'a -> 'a; .. > -> int = <fun>
多相メソッドを使うと、メソッドが任意の部分型を引数として取るようにできます。 3.8 節で述べたように、関数にはあるクラスの任意の部分型を引数に取るものがありました。同じことがメソッドについても実現できます。
#
class type point0 = object method get_x : int end;;
class type point0 = object method get_x : int end
#
class distance_point x = object inherit point x method distance : 'a. ( point0 as 'a) -> int = fun other -> abs (other get_x - x) end;;
class distance_point : int -> object val mutable x : int method distance : #point0 -> int method get_offset : int method get_x : int method move : int -> unit end
#
let p = new distance_point 3 in (p#distance (new point 8), p#distance (new colored_point 1 "blue"));;
- : int * int = (5, 2)
ここで、型変数 'a
が point0
型の部分型を表すことを示すために、 (#point0 as 'a)
という構文を用いました。量化子と同じくこの表記もクラス使用では省略することができます。オブジェクトのフィールド内で多相性を使いたい場合には、それぞれ別個に量化子を指定しなければなりません。
#
class multi_poly = object method m1 : 'a. (< n1 : 'b. 'b -> 'b; .. > as 'a) -> _ = fun o -> o n1 true, o n1 "hello" method m2 : 'a 'b. (< n2 : 'b -> bool; .. > as 'a) -> 'b -> _ = fun o x -> o n2 x end;;
class multi_poly : object method m1 : < n1 : 'a. 'a -> 'a; .. > -> bool * string method m2 : < n2 : 'b -> bool; .. > -> 'b -> bool end
メソッド m1
では o
は 少なくともそれ自体多相的なメソッド n1
をもつオブジェクトでなくてはいけません。一方、メソッド m2
では n2
の引数と x
は同じ型を持たなくてはならず、その型変数は 'a
と同じレベルで量化されています。