前回は多重継承と Mix-in について説明しました。今回は「クラス型」について説明します。
通常、オブジェクトの型は型推論により定義されますが、class 文でクラスを定義するとき、シグネチャのようにクラスのデータ型を明示的に宣言することができます。これを「クラス型」といいます。クラス型を宣言することでインスタンス変数やプライベートメソッドを隠蔽することができます。ただし、公開メソッドや抽象メソッドは隠蔽することができません。
クラス型は次のように指定します。
class クラス名 : object inherit スーパークラス名 ... val インスタンス変数名1 : 型v1 val インスタンス変数名2 : 型v2 ... method メソッド名1 : 型m1 method メソッド名2 : 型m2 ... end = object ... end
モジュールと同様に、クラス名の後ろに : を付けて object ... end の中にインスタンス変数やメソッドの型を定義します。この中で継承 (inherit) を使ってもかまいません。
簡単な例を示しましょう。前回作成したクラス point でクラス型を宣言します。次のリストを見てください。
リスト 1 : point クラスの定義 (* ポイント *) class point xi yi : object ('self_type) method x : float method y : float method move : float -> float -> unit method equal : 'self_type -> bool end = object (self: 'self_type) val mutable x = xi val mutable y = yi method x = x method y = y method move dx dy = x <- x +. dx; y <- y +. dy method equal (p: 'self_type) = x = p#x && y = p#y end (* 色付きのポイント *) class colored_point xi yi (ci: int) : object ('self_type) inherit point method color : int method equal : 'self_type -> bool end = object (self: 'self_type) inherit point xi yi as super val mutable c = ci method color = c method equal (p: 'self_type) = super#equal p && c = p#color end
クラス型を宣言するとき、object ('self_type) のように自分自身の型を指定することができます。これで equal のようなバイナリメソッドの型を定義することができます。クラス point と colored_point の型宣言で、インスタンス変数の型は定義されていません。これでインスタンス変数の情報を隠蔽することができます。
実際にクラスを定義すると、次のように表示されます。
class point : float -> float -> object ('a) method equal : 'a -> bool method move : float -> float -> unit method x : float method y : float end class colored_point : float -> float -> int -> object ('a) method color : int method equal : 'a -> bool method move : float -> float -> unit method x : float method y : float end
このように、クラス型を宣言することでインスタンス変数を隠蔽することができます。
ところで、クラス型はシグネチャのように名前を付けることができます。これをクラスのインターフェースと呼ぶことがあります。クラス型の名前は class type 宣言で定義します。
class type 名前 = object inherit スーパークラス名 ... val インスタンス変数名1 : 型v1 val インスタンス変数名2 : 型v2 ... method メソッド名1 : 型m1 method メソッド名2 : 型m2 ... end
クラス型の名前はクラス名と同様にデータ型の指定や制限に使うことができます。
簡単な例を示します。point クラスの型に名前をつけてみましょう。
リスト 2 : point クラス (2) (* point クラスの型名 *) class type point_type = object ('self_type) method x : float method y : float method move : float -> float -> unit method equal : 'self_type -> bool end (* point クラス *) class point xi yi : point_type = object (self: 'self_type) val mutable x = xi val mutable y = yi method x = x method y = y method move dx dy = x <- x +. dx; y <- y +. dy method equal (p: 'self_type) = x = p#x && y = p#y end (* colored_point の型名 *) class type colored_point_type = object ('self_type) inherit point method color : int method equal : 'self_type -> bool end (* colored_point クラス *) class colored_point xi yi (ci: int) : colored_point_type = object (self: 'self_type) inherit point xi yi as super val mutable c = ci method color = c method equal (p: 'self_type) = super#equal p && c = p#color end
point クラスの型名を point_type とし、colored_point クラスの型名を colored_point_type としました。これを使って、クラスの型を指定することができます。実際にクラスを定義すると次のように表示されます。
class type point_type = object ('a) method equal : 'a -> bool method move : float -> float -> unit method x : float method y : float end class point : float -> float -> point_type class type colored_point_type = object ('a) method color : int method equal : 'a -> bool method move : float -> float -> unit method x : float method y : float end class colored_point : float -> float -> int -> colored_point_type
次は 2 点間の距離を計算する関数 distance を作ります。次のリストを見てください。
リスト 3 : 2 点間の距離を求める let distance p1 p2 = let dx = p1#x -. p2#x and dy = p1#y -. p2#y in sqrt (dx *. dx +. dy *. dy)
プログラムは簡単です。引数 p1 と p2 は点を表すオブジェクトです。メソッド x, y で座標を求め、2 点間の距離を計算するだけです。distance をコンパイルすると、データ型は次のように推論されます。
val distance : < x : float; y : float; .. > -> < x : float; y : float; .. > -> float
引数のデータ型はメソッド x, y を持つ任意のオブジェクトになります。これで、point と colored_point の距離を求めることができます。ところが、それ以外のオブジェクトでも、メソッド x, y が定義されていれば distance を呼び出すことができます。たとえば、3 次元の座標を表す point3d クラスを定義しましょう。
リスト 4 : point3d クラス class point3d xi yi zi = object (self: 'self_type) val mutable x = xi val mutable y = yi val mutable z = zi method x = x method y = y method z = z method move dx dy dz = x <- x +. dx; y <- y +. dy; z <- z +. dz method equal (p: 'self_type) = x = p#x && y = p#y && z = p#z end
point3d はメソッド x, y を持っていて、そのデータ型が point と同じなので、point3d のオブジェクトを distance の引数に渡してもコンパイルエラーにはなりません。そこで、次のように引数のデータ型を制限します。
リスト 5 : 2 点間の距離を求める (2) let distance (p1: point_type) (p2: point_type) = let dx = p1#x -. p2#x and dy = p1#y -. p2#y in sqrt (dx *. dx +. dy *. dy)
distance の引数のデータ型は point_type に制限されます。これで point3d のオブジェクトを渡すとエラーになりますが、これでは colored_point のオブジェクトもエラーになってしまいます。このような場合、point と colored_point の両方を満たす部分型を指定できると便利です。
OCaml の場合、クラス名またはクラス型名の前に # を付けると、そのデータ型の任意の部分型を指定することができます。たとえば、メソッド x, y, move を持つクラス型を定義します。
リスト 6 : クラス型 point0 の定義 class type point0 = object method move : float -> float -> unit method x : float method y : float end
point と colored_point の共通のメソッドを point0 で定義します。すると、#point0 は point0 の任意の部分型を表すことになります。実際にオブジェクトの型を示すと、次のようになります。
point0 : < move float -> float -> unit; x : float; y : float > #point0 : < move float -> float -> unit; x : float; y : float; .. >
#point0 は poit0 の任意の部分型なので、最後にピリオドが 2 つ ( .. ) が付いています。この #point0 を使って distance の引数の型を制限することができます。
リスト 7 : 2 点間の距離を求める (2) let distance (p1: (#point0 as 'a)) (p2: 'a) = let dx = p1#x -. p2#x and dy = p1#y -. p2#y in sqrt (dx *. dx +. dy *. dy)
val distance : (#point0 as 'a) -> 'a -> float
p1 と p2 は同じデータ型であることを示すため、p1 の型指定で #point0 as 'a により p1 に型変数 'a を付けて、第 2 引数 p2 のデータ型は 'a で指定します。p2 の型指定を #point0 とすると、p1 とデータ型が異なるオブジェクト、たとえば、引数のデータ型が point と colored_point でもコンパイルエラーにはなりません。
point と colored_point は point0 の部分型なので、distance で距離を求めることができます。point3d はメソッド move の型が point0 の move と異なるので、point0 の部分型にはなりません。したがって、point3d のオブジェクトを distance に渡すとコンパイルエラーになります。
簡単な実行例を示しましょう。
# let p1 = new point 0. 0. ;; val p1 : point = <obj> # let p2 = new point 10. 10. ;; val p2 : point = <obj> # let p3 = new colored_point 0. 0. 1;; val p3 : colored_point = <obj> # let p4 = new colored_point 5. 5. 1;; val p4 : colored_point = <obj> # distance p1 p2;; - : float = 14.142135623730951 # distance p3 p4;; - : float = 7.0710678118654755
distance p1 p3 とするとエラーになります。
インスタンス変数は個々のインスタンス (オブジェクト) に格納される変数です。その値はインスタンスによって変わります。クラスで共通の変数や定数を使いたい場合は、class の中で変数や定数を定義します。
class クラス名 = let ... 局所変数や局所関数の定義 ... in object ... end
OCaml は class 文の = と object の間に、let ... in で局所変数や局所関数を定義することができます。一般に、クラス共通で使用する変数や定数のことを「クラス変数」や「クラス定数」といいます。OCaml の場合、クラス内で定義された局所変数は、クラス変数やクラス定数として利用することができます。
簡単な例を示しましょう。
リスト 8 : クラス定数とクラス変数 class foo = let name = "foo" in let value = ref 0 in object method show = Printf.printf "%s %d\n" name !value method update x = value := x end
let で定義された局所変数は同じクラスのメソッドからアクセスすることができます。リスト 8 では局所変数 name がクラス定数になり、value がクラス変数になります。クラス変数は値を書き換えることがあるので、value は参照型変数として定義しています。メソッド show は name と value を表示します。メソッド update は value の値を書き換えます。
それでは実際に試してみましょう。
# let a = new foo;; val a : foo = <obj> # let b = new foo;; val b : foo = <obj> # a#show;; foo 0 - : unit = () # b#show;; foo 0 - : unit = () # a#update 10;; - : unit = () # a#show;; foo 10 - : unit = () # b#show;; foo 10 - : unit = ()
2 つのインスタンスを生成して変数 a, b にセットします。どちらのインスタンスもメソッド show で name と value の値を表示すると foo 0 になります。ここで、a#update で value の値を 10 に書き換えます。a#show は foo 10 を表示しますが、b#show も foo 10 を表示します。局所変数 value は 2 つのインスタンスで共有されていることがわかります。
また、foo を継承して新しいクラスを作ると、そのクラスも foo の局所変数を共有します。簡単な例を示しましょう。
# class bar = object inherit foo end;; class bar : object method show : unit method update : int -> unit end # let a = new foo;; val a : foo = <obj> # let b = new bar;; val b : bar = <obj> # a#show;; foo 0 - : unit = () # b#show;; foo 0 - : unit = () # a#update 10;; - : unit = () # a#show;; foo 10 - : unit = () # b#show;; foo 10 - : unit = ()
このように、foo と bar のインスタンスは局所変数 name と value の値を共有しています。ただし、クラス bar のメソッドから foo の局所変数に直接アクセスすることはできません。foo のメソッドを経由してアクセスする必要があります。ご注意くださいませ。
前回作成した可変長配列クラス arraylist を継承して、要素を昇順に並べて格納するクラス sorted_arraylist を作成してください。
リスト : sorted_arraylist クラス (* 例外 *) exception Can_not_use class ['a] sorted_arraylist compare init_size (init_value : 'a) = object(self) inherit ['a] arraylist init_size init_value as super method private move i = if i = 0 then () else let a = self#get (i - 1) in let b = self#get i in if compare a b <= 0 then () else (super#set i a; super#set (i - 1) b; self#move (i - 1)) method push x = super#push x; self#move (self#length - 1) method set i x = raise Can_not_use end
inherit で arraylist を継承するときに別名 super を付けます。メソッド push はスーパークラスのメソッド super#push を呼び出して x を末尾に追加し、プライベートメソッド move で末尾データを適切な位置に挿入します。move は i - 1 番目のデータ a と i 番目のデータ b を比較して、a > b であれば a と b を交換して、次のデータと比較します。i が 0 または a <= b であれば処理を終了します。
メソッド set は例外 Can_not_use を送出するだけです。move でデータを交換するときは super#set で上位クラスのメソッド set を呼び出していることに注意してください。
なお、メソッド move はデータの挿入位置を求めるのに線形探索しているので、データ数が多くなると時間がかかるようになります。データ数を N とすると、実行時間は N に比例します。ここで「二分探索 (binary search)」を使うと、log2N に比例する時間でデータの挿入位置を求めることができます。興味のある方はプログラムを改造してみてください。
簡単な実行例を示します。
class ['a] sorted_arraylist : ('a -> 'a -> int) -> int -> 'a -> object val mutable buff : 'a array val mutable idx : int val mutable top : int method begin0 : unit method count_if : ('a -> bool) -> int method filter : ('a -> bool) -> 'a list method find_if : ('a -> bool) -> 'a option method fold_left : ('b -> 'a -> 'b) -> 'b -> 'b method get : int -> 'a method iter : ('a -> unit) -> unit method length : int method map : ('a -> 'b) -> 'b list method private move : int -> unit method next : 'a option method peek : 'a method pop : 'a method position_if : ('a -> bool) -> int method push : 'a -> unit method set : int -> 'a -> unit end
# let a = new sorted_arraylist compare 5 0;; val a : int sorted_arraylist =# List.iter (fun x -> a#push x) [5; 4; 6; 3; 7; 2; 8; 1; 9; 0];; - : unit = () # a#iter (fun x -> print_int x; print_newline());; 0 1 2 3 4 5 6 7 8 9 - : unit = () # a#length;; - : int = 10 # a#peek;; - : int = 9 # a#pop;; - : int = 9 # a#length;; - : int = 9 # a#get 0;; - : int = 0 # a#set 0 100;; Exception: Can_not_use.