前回まででオブジェクト指向の基本であるクラス、インスタンス、メソッドについて一通り説明しました。今回はオブジェクト指向機能の目玉ともいえる「継承」について説明します。
OCaml の継承は他のオブジェクト指向言語と比べると、基本的な考え方は同じですが、クラスの継承関係が必ずしも「部分型」にはならない場合があるので注意が必要です。まず最初に、一般的なオブジェクト指向言語で使われている継承について簡単に説明します。
「継承 (inheritance : インヘリタンス)」は、簡単に言うとクラスに「親子関係」を持たせる機能です。子供のクラスは親クラスの性質を受け継ぐことができます。プログラミング言語の場合、引き継ぐ性質は定義されたインスタンス変数やメソッドになります。
プログラムを作る場合、いままで作ったプログラムと同じような機能が必要になることがありますが、継承を使うことでその機能を受け継ぎ、新規の機能や変更される機能だけプログラムする、いわゆる「差分プログラミング」が可能になります。
クラスを継承する場合、その元になるクラスを「スーパークラス」とか「ベースクラス」と呼びます。そして、継承したクラスを「サブクラス」と呼びます。この呼び方は言語によってまちまちで統一されていません。C++ の場合は、元になるクラスを基本クラスといい、継承するクラスを派生クラスとか導出クラスといいます。
たとえば、クラス Foo1 を継承してクラス Foo2 を定義しましょう。クラス Foo1 にはメソッド bar が定義されています。クラス Foo2 にメソッド bar は定義されていませんが、Foo2 のオブジェクトに対して bar を呼び出すと、スーパークラス Foo1 のメソッド bar が実行されるのです。
メソッドの選択は次のように行われます。まず、オブジェクトが属するクラス Foo2 にメソッド bar が定義されているか調べます。ところが、Foo2 には bar が定義されていないので、スーパークラスである Foo1 に bar が定義されているか調べます。ここでメソッド bar が見つかり、それを実行するのです。このように、メソッドが見つかるまで順番にスーパークラスを調べていきますが、最上位のスーパークラスまで調べてもメソッドが見つからない場合はエラーとなります。
継承したクラスのメソッドとは違う働きをさせたい場合、同名のメソッドを定義することで、そのクラスのメソッドを設定することができます。これを「オーバーライド (over ride)」といいます。メソッドを選択する仕組みから見た場合、オーバーライドは必然の動作です。メソッドはサブクラスからスーパークラスに向かって探索されるので、スーパークラスのメソッドよリサブクラスのメソッドが先に選択されるわけです。
継承には「単一継承」と「多重継承」の 2 種類があります。単一継承は、ただひとつのクラスからしか機能を継承することができません。したがって、クラスの階層は図 1 のような木構造で表すことができます。
A /|\ / | \ B C D / \ / \ E F 図 1 : 単一継承におけるクラスの階層
継承は何段階に渡って行われてもかまいません。たとえばクラス E の場合、スーパークラスが B で、B のスーパークラスが A に設定されています。サブクラスは複数あってもかまいません。たとえば、A のサブクラスは B, C, D と 3 つ、B のサブクラスは E, F と 2 つあります。図 1 の場合、クラス A のスーパークラスはありませんが、ほかのクラスではただひとつのスーパークラスを持っています。プログラミング言語では、Java や Smalltalk が単一継承です。
これに対し多重継承は、複数のクラスを継承することができます。このため、クラスの階層は木構造ではなく、図 2 のようなグラフ [*1] で表すことができます。
A / \ / \ B C / \ / \ / \ / \ D E F 図 2 : 多重継承におけるクラスの階層
クラス E に注目してください。スーパークラスには B と C の 2 つがあります。多重継承では、単一継承と同じくサブクラスを複数持つことができ、なおかつ、スーパークラスも複数持つことができるのです。プログラミング言語では、C++ や Common Lisp Object System (CLOS) が多重継承をサポートしています。そして、OCaml も多重継承です。
実をいうと、M.Hiroi は多重継承に対してあまりいいイメージを持っていません。私見ですが、多重継承はメリットよりもプログラムを複雑にするデメリットの方が大きいのではないか、と思っています。特に、図 2 のクラス A, B, C, E のような菱形の関係を C++ でプログラムする場合、とても複雑な問題を引き起こすことが知られています。OCaml の場合でも、多重継承で問題が発生することがあります。多重継承については次回で詳しく説明します。
一般的なオブジェクト指向言語の場合、継承によって引き継がれる性質は定義されたインスタンス変数やメソッドになります。図 3 を見てください。
クラス Foo にはインスタンス変数 a, b とメソッド get_a, get_b が定義されています。次に、クラス Bar を定義します。Bar は Foo を継承し、Bar 固有のインスタンス変数 c とメソッド get_c を定義します。Foo と Bar のインスタンスを生成すると、図 3 に示したように、Bar のインスタンスにはクラス Foo で定義された変数 a, b も含まれます。このように、Foo のインスタンス変数が Bar に継承されます。
Foo のインスタンスを生成すると、もちろん変数 a, b は含まれていますが、Bar のインスタンスとメモリを共有することはありません。クラスはオブジェクトの設計図です。設計に共通な部分があったとしても、それから生み出されるインスタンスは別々の実体で、インスタンス変数を共有することはないのです。
クラス Bar にはメソッド get_c しか定義されていませんが、クラス Foo を継承することにより、メソッド get_a と get_b を利用することができます。Bar のインスタンスに対して get_a を呼び出すと、クラス Bar には get_a が定義されていないので、スーパークラス Foo を調べ、そこで定義されている get_a が呼び出されます。もちろん、取り出される値は Bar のインスタンスにある変数 a の値です。このように、Foo のメソッドが Bar に継承されます。
class ┌─ Foo ─┐ ┌─ instance ─┐ ├─────┤ ├───────┤ │ 変数 a │────→│ 変数 a │ ├─────┤ ├───────┤ │ 変数 b │ │ 変数 b │ └─────┘ └───────┘ method : get_a, get_b │ 継承 ↓ ┌─ Bar ─┐ ┌─ instance ─┐ ├─────┤────→├───────┤ │ 変数 c │ │ 変数 a │ └─────┘ ├───────┤ method : get_c │ 変数 b │ ├───────┤ │ 変数 c │ └───────┘ 図 3 : 一般的なオブジェクト指向言語における継承
それでは、具体的に OCaml の継承を説明しましょう。スーパークラスは object ... end の中で inherit 宣言を使って指定します。また、inherit 宣言を複数並べると、多重継承を行うことができます。継承に必要な設定はこれだけです。
簡単な例として、図 3 のクラスを実際にプログラムしてみましょう。まずクラス foo を定義します (リスト 1)。
リスト 1 : クラス foo の定義 class ['a] foo x y = object val a = (x: 'a) val b = (y: 'a) method get_a = a method get_b = b end
インスタンス変数 a, b は引数 x, y で初期化します。データ型は型変数 'a で表します。メソッド get_a と get_b の定義は簡単です。与えられたインスタンスから値を取り出すだけです。次にクラス bar を定義します (リスト 2)。
リスト 2 : クラス Bar の定義 class ['a] bar x y z = object inherit ['a] foo x y val c = (z: 'a) method get_c = c end
inherit でスーパークラス foo を指定します。このとき、必要な型変数や引数を指定します。これで foo で定義されているインスタンス変数が初期化されます。あとは自分のクラスで使うインスタンス変数 c を初期化し、メソッド get_c を定義します。
実際にクラス foo と bar を定義すると、次のように表示されます。
class ['a] foo : 'a -> 'a -> object val a : 'a val b : 'a method get_a : 'a method get_b : 'a end class ['a] bar : 'a -> 'a -> 'a -> object val a : 'a val b : 'a val c : 'a method get_a : 'a method get_b : 'a method get_c : 'a end
クラス foo を継承したことにより、クラス bar には foo のインスタンス変数とメソッドが引き継がれていることが分かります。なお、クラス bar は foo にメソッド get_c を追加しているだけなので、bar は foo の部分型になります。
それでは実行してみましょう。
# let a = new foo 1 2;; val a : int foo = <obj> # let b = new bar 10 20 30;; val b : int bar = <obj> # a#get_a;; - : int = 1 # a#get_b;; - : int = 2 # b#get_a;; - : int = 10 # b#get_b;; - : int = 20 # b#get_c;; - : int = 30
メソッド get_a は bar に定義されていませんが、スーパークラス foo のメソッド get_a が呼び出されて、インスタンス変数 a の値を求めることができます。また、bar のインスタンスに対して get_c を呼び出せば、インスタンス変数 c の値を求めることができます。
継承はクラスに新しい機能を追加するだけではなく、メソッドをオーバーライドすることで機能を変更することができます。簡単な例題として、前々回作成したスタックを継承して、格納する要素数を制限するスタック fixed_stack というクラスを作ってみましょう。リスト 3 を見てください。
リスト 3 : 制限付きスタック exception Full class ['a] fixed_stack (limit: int) = object inherit ['a] stack as super val mutable size = 0 method push x = if size = limit then raise Full else size <- size + 1; super#push x method pop = if size = 0 then raise Empty else size <- size - 1; super#pop method is_full = size = limit end
fixed_stack は指定した上限値までしか要素を格納できません。stack で要素を追加するメソッドは push で、削除するメソッドは pop です。この 2 つのメソッドをオーバーライドすることで、fixed_stack の機能を実現することができます。スタックの上限値は引数 limit で指定し、スタックの要素数はインスタンス変数 size で管理します。
fixed_stack は stack を継承するので、inherit でスーパークラスに stack を指定します。メソッドをオーバーライドするとき、スーパークラスのメソッドを呼び出すことができると便利です。これを「メソッド結合 (method combination)」といいます。OCaml の場合、inherit でスーパークラスを指定しますが、このとき次のように名前を指定すると、その名前を使ってスーパークラスのメソッドを呼び出すことができます。
inherit スーパークラス名 as 名前
fixed_stack では、名前に super を指定しているので、super#push や super#pop でスーパークラス stack のメソッドを呼び出すことができます。
メソッド push は limit と size を比較して、size が limit と等しい場合はデータを挿入できないので例外 Full を送出します。そうでない場合はスーパークラスのメソッド push を呼び出して、データを挿入して size を +1 します。
メソッド pop の場合、size が 0 であればデータを削除できないので例外 Empty を送出します。size が 0 よりも大きいときにスーパークラスのメソッド pop を呼び出して、size を -1 します。これで、スタックに格納される要素数を管理することができます。
簡単な実行例を示しましょう。
# let a = new fixed_stack 5; val a : '_a fixed_stack = <obj> # for i = 0 to 4 do a#push i done;; - : unit = () # a#push 5;; exception: Full # a#is_full;; - : bool = true # while not a#is_empty do print_int a#pop; print_string " " done;; 4 3 2 1 0 - : unit = ()
このように stack を継承することで、fixed_stack を簡単にプログラムすることができます。
もう一つ簡単な例題として、前回作成した集合 set を継承して、要素を昇順に並べて格納する集合 sorted_set を作ってみましょう。sorted_set はリストは要素をまとめてソートするのではなく、要素が昇順に並ぶように適切な位置にデータを挿入します。また、要素を昇順に並べておくと、最小値を簡単に求めることができます。
プログラムをリスト 4 に示します。
リスト 4 : 順序付き集合 exception Empty class ['a] sorted_set compare = object inherit ['a] set compare method member p = let rec mem_eq = function [] -> false | x::xs -> let r = compare p x in if r = 0 then true else if r < 0 then false else mem_eq xs in mem_eq content method insert p = let rec ins = function [] -> [p] | (x::xs) as ls -> let r = compare p x in if r = 0 then ls else if r < 0 then p::ls else x :: (ins xs) in content <- ins content method min = match content with [] -> raise Empty | x::_ -> x method delete_min = match content with [] -> raise Empty | x::xs -> content <- xs; x end
sorted_set は set を継承するので、inherit で ['a] set と指定します。このとき、要素を比較する関数 compare も忘れずに指定してください。オーバーライドするメソッドは member と insert です。データを昇順に並べると、探索処理は線形探索よりも速くなります。メソッド min は最小値のデータを返します。メソッド delete_min は集合から最小値のデータを取り除きます。
member は簡単にプログラムできます。要素は昇順に並んでいるので、引数 p よりも要素 x が大きい場合、それ以降に p と等しい要素は存在しません。ここで探索を打ち切って false を返すことができます。
データを挿入する insert も簡単です。これは拙作のページ「ソート」で取り上げた「挿入ソート」と同じ考え方です。データ p と要素を比較していき、p よりも大きい要素の前に p を挿入します。リストが空リストになった場合、p が一番大きなデータなので [p] を返します。
メソッド min は content の先頭の要素を返すだけです。メソッド delete_min は content の先頭の要素を取り除き、その要素を返します。どちらのメソッドも content が空リストの場合は例外 Empty を送出します。
sorted_list を定義すると、次のように表示されます。
class ['a] sorted_set : ('a -> 'a -> int) -> object ('b) val mutable content : 'a list val mutable size : int method copy : 'b method delete : 'a -> unit method delete_min : 'a method difference : 'b -> 'b method insert : 'a -> unit method intersection : 'b -> 'b method is_equal : 'b -> bool method is_subset : 'b -> bool method iter : ('a -> unit) -> unit method length : int method member : 'a -> bool method min : 'a method union : 'b -> 'b end
set から継承したバイナリメソッドの型に注目してください。引数の型が自分自身 (sorted_set) と同じ 'b になっていますね。もしも、クラス set でバイナリメソッドの引数の型を 'a set とすると、sorted_set のバイナリメソッドの引数の型も 'a set になってしまうので、sorted_set の集合演算を定義することができません。
また、集合演算を行うメソッドでは返り値が 'b になっています。もしも、オブジェクトをコピーしないで新しい集合を new set で生成すると、集合演算の返り値は 'a set になってしまいます。自分自身の型を指定し、オブジェクトをコピーすることで、set を継承した sorted_set を正しく定義することができます。
ただし、問題点がないわけではありません。実は、sorted_set は set の部分型にはなりません。このことについては次回で詳しく説明します。
それでは簡単な実行例を示しましょう。
# let print s = s#iter (fun x -> print_int x#get; print_string " ");; val print : < iter : (int -> unit) -> 'a; .. > -> 'a = <fun> # let a = new sorted_set compare;; val a : '_a sorted_set = <obj> # for i = 1 to 5 do a#insert i done;; - : unit = () # print a;; 1 2 3 4 5 - : unit = () # let b = new sorted_set compare;; val b : '_a sorted_set = <obj> # for i = 4 to 8 do b#insert i done;; - : unit = () # print b;; 4 5 6 7 8 - : unit = () # print (a#union b);; 1 2 3 4 5 6 7 8 - : unit = () # print (a#intersection b);; 4 5 - : unit = () # print (a#difference b);; 1 2 3 - : unit = ()
正常に動作していますね。
インスタンスを生成した後で独自の処理を行いたい場合は initializer を設定します。
initializer 式
initializer はインスタンスが生成されてから呼び出されます。このとき、インスタンス変数の値は初期化されています。式の返り値は unit でなければいけません。また、initializer の中で自分のクラスのメソッドを呼び出すこともできます。簡単な例を示しましょう。
リスト 5 : initializer class foo (xi: int) = object val mutable x = xi initializer print_int x; print_newline () method get_x = x method set_x a = x <- a end class bar (xi: int) (yi: int) = object inherit foo xi val mutable y = yi initializer print_int y; print_newline () method get_y = y method set_y a = y <- a end
クラス foo と bar の initializer はインスタンス変数の値を表示します。initializer はオーバーライドされることはなく、継承したクラスの initializer は設定されていれば必ず実行されます。
簡単な実行例を示します。
# let a = new foo 10;; 10 val a : foo = <obj> # let b = new bar 1 2;; 1 2 val b : bar = <obj>
bar のインスタンスを生成すると、foo の initializer が評価されて、その次に bar の initializer が評価されていることがわかります。
クラスでメソッドを定義するとき、キーワード virtual を付けるとメソッドの型だけを宣言することができます。これを「抽象メソッド (virtual method)」といいます。そして、抽象メソッドを持つクラスを「抽象クラス (virtual class)」といい、new でインスタンスを生成することはできません。抽象クラスと抽象メソッドは次のように定義します。
class virtual クラス名 args ... = object method virtual メソッド名 : 型式 ... end
抽象クラスは継承されることを前提としたクラスで、抽象メソッドはサブクラスにおいて具体的に定義されます。抽象クラスでは、サブクラス共通のメソッドを定義します。このとき、抽象クラスのメソッドは抽象メソッドを使って定義することができます。サブクラスのインスタンスが生成されるとき、そのサブクラスでは抽象メソッドが具体化されているはずなので、抽象クラスのメソッドからサブクラスのメソッドが呼び出されることになります。
それでは簡単な例題として、図形の面積を求めるプログラムを作ってみましょう。次のリストを見てください。
リスト 6 : 図形のクラス let pi = 3.14159265 (* 抽象クラス *) class virtual figure = object (self) method virtual kind_of : string method virtual area : float method print = Printf.printf "%s: area = %f\n" self#kind_of self#area end
クラス figure は抽象クラスです。メソッド kind_of と area が抽象メソッドで、kind_of は図形の種類を文字列で返し、area は図形の面積を計算して返します。print は図形の種別と面積を表示するメソッドです。ここで、抽象メソッド kind_of と area を呼び出しています。kind_of と area はサブクラスで定義します。
実際に figure を定義すると、次のように表示されます。
class virtual figure : object method virtual area : float method virtual kind_of : string method print : unit end
次は図形を表すサブクラスを定義します。
リスト 7 : サブクラスの定義 (* 三角形 *) class triangle altitude base = object inherit figure method get_altitude = altitude method get_base = base method kind_of = "Triangle" method area = altitude *. base /. 2.0 end (* 四角形 *) class rectangle width height = object inherit figure method get_width = width method get_height = height method kind_of = "Rectangle" method area = width *. height end (* 円 *) class circle radius = object inherit figure method get_radius = radius method kind_of = "Circle" method area = radius *. radius *. pi end
triangle, rectangle, circle は figure を継承します。サブクラス固有のメソッドも定義されていますが、どのクラスも抽象メソッド kind_of と area を具体化しています。なお、スーパークラスの抽象メソッドをすべて具体化しないと、そのサブクラスも抽象クラスになるため、インスタンスを生成することができません。ご注意ください。
実際にクラスを定義すると、次のように表示されます。
class triangle : float -> float -> object method area : float method get_altitude : float method get_base : float method kind_of : string method print : unit end class rectangle : float -> float -> object method area : float method get_height : float method get_width : float method kind_of : string method print : unit end class circle : float -> object method area : float method get_radius : float method kind_of : string method print : unit end
それでは簡単な実行例を示します。
# let a = new triangle 2.0 2.0;; val a : triangle = <obj> # let b = new rectangle 2.0 2.0;; val b : rectangle = <obj> # let c = new circle 2.0;; val c : circle = <obj> # a#print;; Triangle: area = 2.000000 - : unit = () # b#print;; Rectangle: area = 4.000000 - : unit = () # c#print;; Circle: area = 12.566371
正常に動作していますね。
OCaml では、型 A の部分型のオブジェクトは型 A のオブジェクトに変換することができます。この機能を「型変換 (coercion)」といいます。これは Java などのオブジェクト指向言語の「アップキャスト」に相当します。なお、OCaml のオブジェクト指向には「ダウンキャスト」に相当する機能はありません。型変換の構文を示します。
(式 : 型1 :> 型2)
式の評価値 (オブジェクト) の型 1 を型 2 に変換します。なお、たいていの場合は ": 型1" の部分を省略することができます。
たとえば、図形のオブジェクトをリストにまとめて格納することを考えます。triangle, rectangle, circle は型が違うので、同じリストに格納することはできません。この場合、figure に型変換すると同じリストに格納することができます。次の例を見てください。
# let sum_of_figure ls = List.fold_left (fun x y -> x +. y#area) 0.0 ls;; val sum_of_figure : < area : float; .. > list -> float = <fun> # sum_of_figure [(a :> figure); (b :> figure); (c :> figure)];; - : float = 18.5663706
関数 sum_of_figure は図形の面積の合計値を求めます。型変換した場合、サブクラスの情報は失われるため、サブクラス独自のメソッドを呼び出すことはできません。型変換したスーパークラスのメソッドしか利用できませんが、ポリモーフィズムによりサブクラスのメソッドが呼び出されるため、図形の面積を正しく計算することができます。
可変長の配列を表すクラス arraylist を定義してください。
リスト : 可変長配列クラス arraylist exception Empty exception Out_of_range class ['a] arraylist init_size (init_value : 'a) = object val mutable buff = Array.make init_size init_value val mutable top = 0 method push x = if top >= Array.length buff then let newbuff = Array.make (2 * Array.length buff) init_value in Array.blit buff 0 newbuff 0 (Array.length buff); buff <- newbuff else (); buff.(top) <- x; top <- top + 1 method pop = if top = 0 then raise Empty else (top <- top - 1; buff.(top)) method peek = if top = 0 then raise Empty else buff.(top - 1) method get i = if i < 0 || i >= top then raise Out_of_range else buff.(i) method set i x = if i < 0 || i >= top then raise Out_of_range else buff.(i) <- x method length = top end
exception Empty exception Out_of_range class ['a] arraylist : int -> 'a -> object val mutable buff : 'a array val mutable top : int method get : int -> 'a method length : int method peek : 'a method pop : 'a method push : 'a -> unit method set : int -> 'a -> unit end
インスタンス変数 buff に配列本体を、インスタンス変数 top はスタックトップの位置を表します。これは配列の要素数と同じで、0 に初期化します。メソッド push は x を追加するとき、buff の大きさと top を比較します。同じ値であれば満杯なので x を追加することはできません。大きさが 2 倍の配列を生成して変数 newbuff にセットします。それから、buff の要素を newbuff にコピーします。これは Array の関数 blit を使うと簡単です。
# Array.blit;; - : 'a array -> int -> 'a array -> int -> int -> unit = <fun>
blit a1 s1 a2 s2 size は配列 a1 の s1 番目から size 個の要素を配列 a2 の s2 番目以降にコピーします。簡単な使用例を示します。
# let a = [|1; 2; 3; 4; 5|];; val a : int array = [|1; 2; 3; 4; 5|] # let b = Array.make 10 0;; val b : int array = [|0; 0; 0; 0; 0; 0; 0; 0; 0; 0|] # Array.blit a 0 b 0 5;; - : unit = () # b;; - : int array = [|1; 2; 3; 4; 5; 0; 0; 0; 0; 0|] # Array.blit a 0 b 5 5;; - : unit = () # b;; - : int array = [|1; 2; 3; 4; 5; 1; 2; 3; 4; 5|]
あとは newbuff を buff にセットし、top の位置に x を書き込んでから top の値を +1 します。
メソッド pop は top の値を -1 してから、buff.(top) の値を返します。メソッド peek は buff.(top - 1) の値を返します。top が 0 の場合、配列は空なので例外 Empty を送出します。メソッド get, set は簡単ですね。添字 i が範囲外であれば例外 Out_of_range を送出します。メソッド length は top の値を返すだけです。
それでは実際に試してみましょう。
# let a = new arraylist 4 0;; val a : int arraylist = <obj> # a#length;; - : int = 0 # for i = 1 to 8 do a#push i done;; - : unit = () # a#length;; - : int = 8 # for i = 0 to 7 do print_int (a#get i); print_newline() done;; 1 2 3 4 5 6 7 8 - : unit = () # for i = 0 to 7 do a#set i (10 * a#get i) done;; - : unit = () # for i = 0 to 7 do print_int (a#get i); print_newline() done;; 10 20 30 40 50 60 70 80 - : unit = () # for i = 1 to 8 do print_int (a#pop); print_newline() done;; 80 70 60 50 40 30 20 10 - : unit = () # a#length;; - : int = 0 # a#get 0;; Exception: Out_of_range. # a#set 0 100;; Exception: Out_of_range. # a#pop;; Exception: Empty.
どうやら正常に動作しているようです。興味のある方はいろいろ試してみてください。