Previous Contents Next
Chapter 3 Objects in Caml

(Chapter written by Jerome Vouillon and Didier Remy)





この章では、Objective Caml のオブジェクト指向機能について概観します。

3.1 Classes and objects
3.2 Reference to self
3.3 Initializers
3.4 Virtual methods
3.5 Private methods
3.6 Class interfaces
3.7 Inheritance
3.8 Multiple inheritance
3.9 Parameterized classes
3.10 Polymorphic methods
3.11 Using coercions
3.12 Functional objects
3.13 Cloning objects
3.14 Recursive classes
3.15 Binary methods
3.16 Friends


3.1 Classes and objects

下に示されたクラス point は、ひとつのインスタンス変数 x と二つのメソッド get_xmove を定義しています。 インスタンス変数 x の初期値は 0 です。 x は mutable と宣言されているので move メソッドは x の値を変更できます。
#class point =
   object 
     val mutable x = 0
     method get_x = x
     method move d = x <- x + d
   end;;
class point :
  object val mutable x : int method get_x : int method move : int -> unit end
point クラスのインスタンスである「点」 p を生成してみましょう。
#let p = new point;;
val p : point = <obj>
p の型は point であることに注意して下さい。 これは上のクラス定義の際に自動的に定められた略記です。 型 point はオブジェクト型 <get_x : int; move : int -> unit> を意味します。 オブジェクトの型は、そのオブジェクトが属するクラスが持つメソッドとその型を並べたものになります。 p のメソッドを呼び出してみます。
#p#get_x;;
- : int = 0
 
#p#move 3;;
- : unit = ()
 
#p#get_x;;
- : int = 3
クラス定義本体の評価は、オブジェクトが生成される時に行われます。 そこで以下の例では、 インスタンス変数 x は異なったオブジェクトで異なった値に初期化されます。
#let x0 = ref 0;;
val x0 : int ref = {contents = 0}
 
#class point =
   object 
     val mutable x = incr x0; !x0
     method get_x = x
     method move d = x <- x + d
   end;;
class point :
  object val mutable x : int method get_x : int method move : int -> unit end
 
#new point#get_x;;
- : int = 1
 
#new point#get_x;;
- : int = 2
クラス pointx の初期値を引数として取るよう定義することもできます。
#class point = fun x_init -> 
   object 
     val mutable x = x_init
     method get_x = x
     method move d = x <- x + d
   end;;
class point :
  int ->
  object val mutable x : int method get_x : int method move : int -> unit end
関数定義と同じように、次のように書くこともできます。
#class point x_init =
   object 
     val mutable x = x_init
     method get_x = x
     method move d = x <- x + d
   end;;
class point :
  int ->
  object val mutable x : int method get_x : int method move : int -> unit end
こうすると、クラス point のインスタンスは x の初期値を引数として取り、あたらしい「点」オブジェクトを返す関数になります。
#new point;;
- : int -> point = <fun>
 
#let p = new point 7;;
val p : point = <obj>
引数 x_init はクラス定義の全体で参照することができます。 例えば、以下で定義された get_offset メソッドは現在のオブジェクトの「位置」を初期位置から測った値を返します。
#class point x_init =
   object 
     val mutable x = x_init
     method get_x = x
     method get_offset = x - x_init
     method move d = x <- x + d 
   end;;
class point :
  int ->
  object
    val mutable x : int
    method get_offset : int
    method get_x : int
    method move : int -> unit
  end
クラスのオブジェクト定義本体の前に、式を評価して変数に束縛することができます。 これはオブジェクトがある一定の性質を満たすようにするのに便利です。 次の例では、「点」を一番近くの格子点にあわせるようにしています。
#class adjusted_point x_init =
   let origin = (x_init / 10) * 10 in
   object 
     val mutable x = origin
     method get_x = x
     method get_offset = x - origin
     method move d = x <- x + d
   end;;
class adjusted_point :
  int ->
  object
    val mutable x : int
    method get_offset : int
    method get_x : int
    method move : int -> unit
  end
(また、x_init 座標が格子点上にない時に、例外を発生するようにもできるでしょう。) 同じ効果は、point クラス定義を origin の値で呼び出すことによっても実現できます。
#class adjusted_point x_init =  point ((x_init / 10) * 10);;
class adjusted_point : int -> point
別の方法として、特別な生成関数を定義することも考えられます。
#let new_adjusted_point x_init = new point ((x_init / 10) * 10);;
val new_adjusted_point : int -> point = <fun>
しかし、前に述べたやり方が普通よいのです。 なぜならば、格子点上にくるよう調節する機能がクラス定義の一部になっていて、継承することができるからです。

これは、他のプログラミング言語でいうところのクラス・コンストラクタに相当します。 このようにして、同じクラスに属するオブジェクトを異なったやり方で初期化するコンストラクタを定義することができます。 同様の機能を実現する別の方法として、3.3 節で述べる initializer を用いる方法があります。

3.2 Reference to self

メソッドや initializer は自分自身、つまり現在のオブジェクトのメソッドを呼び出すことができます。 このためには、ここで変数 s になされているように、自分自身を明示的にある変数に束縛する必要があります。 (s はどんな識別子でも構いませんが、 ここではよく self という名前を選びます。)
#class printable_point x_init =
   object (s)
     val mutable x = x_init
     method get_x = x
     method move d = x <- x + d
     method print = print_int s#get_x
   end;;
class printable_point :
  int ->
  object
    val mutable x : int
    method get_x : int
    method move : int -> unit
    method print : unit
  end
 
#let p = new printable_point 7;;
val p : printable_point = <obj>
 
#p#print;;
7- : unit = ()
プログラムが実行されるときには、s はメソッドの呼び出し時に束縛されます。 特にクラス printable_point が継承された場合、 s はそのサブクラスのオブジェクトに束縛されます。
3.3 Initializers

クラス定義内の let-束縛はオブジェクトが生成される前に評価されます。 しかし、式をオブジェクトが生成された直後に評価することも可能です。 そのような式は、initializer と呼ばれる匿名の隠されたメソッドとして表現されます。 このため、自分自身とインスタンス変数を参照することができます。
#class printable_point x_init =
   let origin = (x_init / 10) * 10 in
   object (self)
     val mutable x = origin
     method get_x = x
     method move d = x <- x + d
     method print = print_int self#get_x
     initializer print_string "new point at "; self#print; print_newline()
   end;;
class printable_point :
  int ->
  object
    val mutable x : int
    method get_x : int
    method move : int -> unit
    method print : unit
  end
 
#let p = new printable_point 17;;
new point at 10
val p : printable_point = <obj>
initializer は継承の際に上書きされず、すべて評価されます。 initializer もオブジェクトがある一定の性質をみたすようにするのに便利です。 他の例が 5.1 節に挙げられています。.

3.4 Virtual methods

virtual というキーワードを用いることで、メソッドを宣言だけして定義しないですますことができます。このようなメソッドのことを仮想メソッドと呼びます。 仮想メソッドの定義は後にサブクラスで与えることができます。 仮想メソッドを含むクラスを仮想クラスと呼びます。 仮想クラスを定義する際には、クラス定義にも virtual キーワードが必要です。 また、仮想クラスのインスタンス、つまりこのクラスのオブジェクトを生成することはできません。 一方、仮想クラスの定義は、通常のクラスと同じく型名を定義します。 仮想クラスに対応する型では仮想メソッドは、 通常のメソッドと同じように扱われます。
#class virtual abstract_point x_init =
   object (self)
     val mutable x = x_init
     method virtual get_x : int
     method get_offset = self#get_x - x_init
     method virtual move : int -> unit
   end;;
class virtual abstract_point :
  int ->
  object
    val mutable x : int
    method get_offset : int
    method virtual get_x : int
    method virtual move : int -> unit
  end
 
#class point x_init =
   object
     inherit abstract_point x_init
     method get_x = x
     method move d = x <- x + d 
   end;;
class point :
  int ->
  object
    val mutable x : int
    method get_offset : int
    method get_x : int
    method move : int -> unit
  end
3.5 Private methods

プライベート・メソッドとはオブジェクトのインターフェースに現れないメソッドです。 プライベート・メソッドは同じオブジェクトの他のメソッドからのみ呼び出すことができます。
#class restricted_point x_init =
   object (self)
     val mutable x = x_init
     method get_x = x
     method private move d = x <- x + d
     method bump = self#move 1
   end;;
class restricted_point :
  int ->
  object
    val mutable x : int
    method bump : unit
    method get_x : int
    method private move : int -> unit
  end
 
#let p = new restricted_point 0;;
val p : restricted_point = <obj>
 
#p#move 10;;
This expression has type restricted_point
It has no method move
 
#p#bump;;
- : unit = ()
プライベート・メソッドは継承されます。 つまり、サブクラスから呼び出すことができますが、 後で述べるようにsignature によって隠蔽することもできます。
また、逆にサブクラスでプライベート・メソッドを公開することもできます。
#class point_again x =
   object (self)
     inherit restricted_point x
     method virtual move : _
   end;;
class point_again :
  int ->
  object
    val mutable x : int
    method bump : unit
    method get_x : int
    method move : int -> unit
  end
ここで virtual が用いられているのは、メソッドをその定義をあたえることなく言及するためです。 private キーワードを用いなかったのでメソッドは公開され、その定義は親クラスのものが用いられます。

次のような定義を用いることもできます。
#class point_again x =
   object (self : < move : _; ..> )
     inherit restricted_point x
   end;;
class point_again :
  int ->
  object
    val mutable x : int
    method bump : unit
    method get_x : int
    method move : int -> unit
  end
self の型が move メソッドが公開されていることを示しています。 これでプライベート・メソッドを公開することができます。

プライベート・メソッドはサブクラスでも公開されるべきでないと考えるかも知れません。 しかし、プライベート・メソッドはサブクラスから呼び出すことはできるので、 結局プライベート・メソッドを呼び出す公開メソッドはいつでも定義できます。 例えば次のようにすることができます。
#class point_again x =
   object
     inherit restricted_point x as super
     method move = super#move 
   end;;
class point_again :
  int ->
  object
    val mutable x : int
    method bump : unit
    method get_x : int
    method move : int -> unit
  end
プライベート・メソッドは仮想メソッドでもありえます。 このとき、キーワードは method private virtual の順で与えられなければなりません。

3.6 Class interfaces

クラス・インターフェースはクラス定義から自動的に導かれますが、直接定義してクラスの型を制限するのに用いることができます。 クラス定義と同じく、 クラス・インターフェースの定義も新しい型の略記を定義します。
#class type restricted_point_type = 
   object
     method get_x : int
     method bump : unit
 end;;
class type restricted_point_type =
  object method bump : unit method get_x : int end
 
#fun (x : restricted_point_type) -> x;;
- : restricted_point_type -> restricted_point_type = <fun>
プログラムを文書化する目的に加えて、クラス・インターフェースはクラスの型を制限するために用いられます。 インスタンス変数と、仮想的でないプライベート・メソッドはクラス型を制限することにより隠蔽することができます。 一方、公開メソッドと仮想メソッドは隠蔽できません。
#class restricted_point' x = (restricted_point x : restricted_point_type);;
class restricted_point' : int -> restricted_point_type
Or, equivalently:
#class restricted_point' = (restricted_point : int -> restricted_point_type);;
class restricted_point' : int -> restricted_point_type
クラス・インターフェースはモジュールの signature によって指定することもできます。
#module type POINT = sig 
   class restricted_point' : int ->
     object    
       method get_x : int
       method bump : unit
     end 
 end;;
module type POINT =
  sig
    class restricted_point' :
      int -> object method bump : unit method get_x : int end
  end
 
#module Point : POINT = struct 
   class restricted_point' = restricted_point
 end;;
module Point : POINT
3.7 Inheritance

継承を説明するため、「点」のクラスから継承によって定義された、「色付きの点」のクラスを定義してみましょう。 このクラスは point クラスのすべてのインスタンス変数とメソッドに加えて新しいインスタンス変数である c とメソッドの color を持っています。
#class colored_point x (c : string) =
   object 
     inherit point x
     val c = c
     method color = c
   end;;
class colored_point :
  int ->
  string ->
  object
    val c : string
    val mutable x : int
    method color : string
    method get_offset : int
    method get_x : int
    method move : int -> unit
  end
 
#let p' = new colored_point 5 "red";;
val p' : colored_point = <obj>
 
#p'#get_x, p'#color;;
- : int * string = (5, "red")
「点」は color メソッドを持たないので「点」と「色付きの点」は異なった型を持っています。 しかし、次の get_x 関数は get メソッドをもつ任意のオブジェクト p に適用することができます。 また pget 以外のメソッドを持つこともできます。 これは get_x の型に含まれる .. によって表されています。 ですから、get_x は「点」と「色付きの点」の両方に適用できます。
#let get_succ_x p = p#get_x + 1;;
val get_succ_x : < get_x : int; .. > -> int = <fun>
 
#get_succ_x p + get_succ_x p';;
- : int = 8
次の例が示すように、メソッドはあらかじめ定義されている必要はありせん。
#let set_x p = p#set_x;;
val set_x : < set_x : 'a; .. > -> 'a = <fun>
 
#let incr p = set_x p (get_succ_x p);;
val incr : < get_x : int; set_x : int -> 'a; .. > -> 'a = <fun>
3.8 Multiple inheritance

多重継承をすることもできます。 同じメソッドの定義の内、最後のものだけが有効です。 もしサブクラスで参照可能な親クラスのメソッドがサブクラスで再び定義された場合、 メソッドはサブクラスのものに上書きされます。 しかし、親クラスの定義も、親クラスを識別子に束縛することにより用いることができます。 次の例では、super が親クラスである printable_point に束縛されています。 super は疑似的な変数名で、 super#print のように親クラスのメソッドを呼び出すためにのみ用いることができます。
#class printable_colored_point y c = 
   object (self)
     val c = c
     method color = c
     inherit printable_point y as super
     method print =
       print_string "(";
       super#print;
       print_string ", ";
       print_string (self#color);
       print_string ")"
   end;;
class printable_colored_point :
  int ->
  string ->
  object
    val c : string
    val mutable x : int
    method color : string
    method get_x : int
    method move : int -> unit
    method print : unit
  end
 
#let p' = new printable_colored_point 17 "red";;
new point at (10, red)
val p' : printable_colored_point = <obj>
 
#p'#print;;
(10, red)- : unit = ()
親クラスのプライベート・メソッドで、サブクラスから隠蔽されているものは継承によって上書きされません。 initializer はプライベート・メソッドと見なされるので、 サブクラスに至るクラス階層で定義されたすべての initializer は、それが導入された順に評価されます。

3.9 Parameterized classes

参照セルはオブジェクトとして実装できます。 しかし、素朴な定義は型チェックを通過できません。
#class ref x_init =
   object 
     val mutable x = x_init
     method get = x
     method set y = x <- y
   end;;
Some type variables are unbound in this type:
  class ref :
    'a ->
    object val mutable x : 'a method get : 'a method set : 'a -> unit end
The method get has type 'a where 'a is unbound
その理由は、保存された値の型が指定されていないためメソッドの一つが多相型であるためです。 したがって、保存される値の型をクラスがパラメータとして持つか、クラスの型が単相型に制限されなければなりません。単相型に制限するには次のようにします。
#class ref (x_init:int) =
   object 
     val mutable x = x_init
     method get = x
     method set y = x <- y
   end;;
class ref :
  int ->
  object val mutable x : int method get : int method set : int -> unit end
型変数をパラメータとして持つクラス定義をあたえるには、その型変数を明示的に与える必要があります。 クラスの型変数はかならず [] に囲まれた部分に列挙されなくてはいけません。 多相型を持つオブジェクトの型は、 この型変数を使って制限されなければいけません。 型変数をパラメータとして持つクラスをパラメータ付きクラスと呼びます。
#class ['a] ref x_init = 
   object 
     val mutable x = (x_init : 'a)
     method get = x
     method set y = x <- y
   end;;
class ['a] ref :
  'a -> object val mutable x : 'a method get : 'a method set : 'a -> unit end
 
#let r = new ref 1 in r#set 2; (r#get);;
- : int = 2
型変数が持ちうる型はクラス定義の本体において制限されているかもしれません。 クラス型においては、型変数にあたえられた制限は constraint 節によって表されます。
#class ['a] ref_succ (x_init:'a) = 
   object
     val mutable x = x_init + 1
     method get = x
     method set y = x <- y
   end;;
class ['a] ref_succ :
  'a ->
  object
    constraint 'a = int
    val mutable x : int
    method get : int
    method set : int -> unit
  end
もっと複雑な例を考えましょう。 「点」オブジェクトと、その任意のサブオブジェクトを中心とする「円」を定義することを考えましょう。 move メソッドの型は明示的に指定しなくてはいけません。 というのも、すべての型変数がクラスの型変数として列挙されていなければならないからです。
#class ['a] circle (c : 'a) =
   object 
     val mutable center = c
     method center = center
     method set_center c = center <- c
     method move = (center#move : int -> unit)
   end;;
class ['a] circle :
  'a ->
  object
    constraint 'a = < move : int -> unit; .. >
    val mutable center : 'a
    method center : 'a
    method move : int -> unit
    method set_center : 'a -> unit
  end
他にも、以下に示すように constraint 節を用いて circle クラスを定義することもできます。 constraint 節で用いられている #point という型は、 point クラスを定義する時に自動的に定義されます。 この型は point クラスの任意のサブクラスに属するオブジェクトに合致します。 #point は実際には < get_x : int; move : int -> unit; .. > という型の略記です。 この表記を用いると、 次のような circle クラスの定義を得ることができます。 このクラス定義は、引数に対して若干以前よりも強い制限を科します。 というのは centerget_x メソッドを持つことを求めているからです。
#class ['a] circle (c : 'a) =
   object 
     constraint 'a = #point
     val mutable center = c
     method center = center
     method set_center c = center <- c
     method move = center#move
   end;;
class ['a] circle :
  'a ->
  object
    constraint 'a = #point
    val mutable center : 'a
    method center : 'a
    method move : int -> unit
    method set_center : 'a -> unit
  end
colored_circle クラスは circle クラスの特別な場合で、 「中心」の型が #colored_point に合致することを求めます。 また、color メソッドを持っています。 パラメータ付きクラスを継承する場合、型変数を再び明示的に与える必要があります。 型変数は、以前と同様に [] の間に書かれます。
#class ['a] colored_circle c =
   object
     constraint 'a = #colored_point
     inherit ['a] circle c
     method color = center#color
   end;;
class ['a] colored_circle :
  'a ->
  object
    constraint 'a = #colored_point
    val mutable center : 'a
    method center : 'a
    method color : string
    method move : int -> unit
    method set_center : 'a -> unit
  end
3.10 Polymorphic methods

上記のように、 パラメータ付きクラスは異なった型の値をその内容として保持するインスタンスを持つことができました。 しかし、これはメソッドを多相型にするには不十分です。

典型的な例は反復子です。
#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 メソッドを多相型にしたいのです。 これは、メソッド定義で明示的に多相型を指定することにより可能になります。
#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 "
ここでコンパイラが生成したクラス型を見て分かるように、 クラス定義では多相型メソッドの型変数は明示的に量化されなくてはいけませんが、 クラス型ではその必要はありません。

一方、もし継承や自分自身への型制限によってメソッドの型が分っている時には、 型を完全に省略することができます。 この例ではメソッドを上書きしています。
#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 = < empty : bool; fold : 'a. ('a -> int -> 'a) -> 'a -> 'a >
but is here used with type
  < empty : bool; fold : (int -> int -> int) -> int -> 'b >
この問題は簡単に解決できます。引数の型を制限すればよいのです。
#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.7 節で述べたように、 関数にはあるクラスの任意の部分型を引数に取るものがありました。 同じことがメソッドについても実現できます。
#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)
ここで、型変数 'apoint0 型の部分型を表すことを示すために、 (#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 は同じ型を持たなくてはならず、 その型変数 'b は同じ量化子で束縛されています。

3.11 Using coercions

型変換はかならず明示的に行われます。 変換を行う方法は二つあります。 もっとも一般的な方法では、変換前の型と変換後の型を共に明示的に指定します。

以前見たように、「点」と「色付きの点」は異なった型を持っていました。 例えば、この二つを同じリストの要素にすることはできません。 しかし、「色付きの点」の color メソッドを隠蔽して「点」に変換することができます。
#let colored_point_to_point cp = (cp : colored_point :> point);;
val colored_point_to_point : colored_point -> point = <fun>
 
#let p = new point 3 and q = new colored_point 4 "blue";;
val p : point = <obj>
val q : colored_point = <obj>
 
#let l = [p; (colored_point_to_point q)];;
val l : point list = [<obj>; <obj>]
t のオブジェクトを型 t' に変換できるのは、 tt' の部分型であるときだけです。 例えば、「点」を「色付きの点」に変換することはできません。
#(p : point :> colored_point);;
Type point = < get_offset : int; get_x : int; move : int -> unit >
is not a subtype of type
  colored_point =
    < color : string; get_offset : int; get_x : int; move : int -> unit > 
実際、部分型への変換は安全ではないかもしれないので、 実行時に型を検査する必要があります。 しかし、そのような機構は言語の中に存在しません。

部分型と継承とは何の関係もないことに注意して下さい。 継承はあるクラスを別のクラスから生成するための構文上の機構です。 一方、部分型は型の間の意味上の関連です。 例えば、「色付きの点」のクラスを「点」からの継承を用いずに、 直接定義することもできます。 この場合でも「色付きの点」の型は変化せず、よって「点」の部分型になります。

型変換を行う場合、型変換前の型は省略することができます。 例えば、次のような定義が可能です。
#let to_point cp = (cp :> point);;
val to_point : #point -> point = <fun>
この場合、関数 colored_point_to_point は関数 to_point の特殊な場合になっています。 しかし、いつもそうだとは限りません。 型変換前の型を明示的に指定する方がより正確ですし、 型変換前の型を省略できないこともあります。 例えば、次のクラスを考えましょう。
#class c0 = object method m = {< >} method n = 0 end;;
class c0 : object ('a) method m : 'a method n : int end
型 c は <m : 'a; n : int> as 'a の略記になります。 ここで、次のようなクラス型を定義します。
#class type c1 =  object method m : c1 end;;
class type c1 = object method m : c1 end
型 c1 は <m : 'a> as 'a の略記になります。 型 c から c1へ型変換を行うことができます。
#fun (x:c0) -> (x : c0 :> c1);;
- : c0 -> c1 = <fun>
しかし、型変換の際に変換前の型を省略することはできせん。
#fun (x:c0) -> (x :> c1);;
This expression cannot be coerced to type c1 = < m : c1 >; it has type
  c0 = < m : c0; n : int > as 'a
but is here used with type 'a
Type c0 = 'a is not compatible with type 'a 
Type c0 = 'a is not compatible with type c1 = < m : c1 > 
Only the first object type has a method n.
This simple coercion was not fully general. Consider using a double coercion.
この場合、明示的な指定を用いる必要があります。 場合によっては、クラス型の定義を変えることによってもこの問題は解決できます。
#class type c2 =  object ('a) method m : 'a end;;
class type c2 = object ('a) method m : 'a end
 
#fun (x:c0) -> (x :> c2);;
- : c0 -> c2 = <fun>
c1c2 のクラス型はことなっていますが、 c1c2 は同じ型を定義します。 というのは、同じ型の同じメソッドを持つからです。 しかし、もし型変換前の型が省略され、 型変換後の型が知られているクラス型から導かれている時、 型ではなくクラス型が型変換関数を導くのに用いられます。 このことによって、部分型から上位型への変換の際には、 ほとんどの場合、型変換前の型は明示的に指定する必要がありません。 型変換関数の型は次のようになります。
#let to_c1 x = (x :> c1);;
val to_c1 : < m : #c1; .. > -> c1 = <fun>
 
#let to_c2 x = (x :> c2);;
val to_c2 : #c2 -> c2 = <fun>
二つの型変換関数の型の違いに注目して下さい。 2つめの例では、 型 #c2 = < m : 'a; .. > as 'a は再帰的な型定義になっています。 というのは、クラス型が再帰的に型変数を用いているからです。 よって、c0 クラスのオブジェクトにも適応できます。 一方、最初の例では、c1 は2回展開されて型 < m : < m : c1; .. >; .. > になり、再帰的な型にはなりません。 (#c1 = < m : c1; .. > であることを思い出して下さい。) また、to_c2 の型が #c2 -> c2 である一方、 to_c1 の型はそれより一般的な型を持つことに気づかれたかも知れません。 しかし、3.15節で説明するように型 #c を満たすクラスが c のサブクラスになるとは限らないので、上の性質はいつも成り立つとは限りません。 それでも、パラメータ付きクラスでない場合には、(_ :> c)(_ : #c :> c) よりいつも一般的です。

よく生じる問題として、 クラス c の定義の中で、 クラス c への型変換を行うにはどうすればよいか、 という問題があります。 問題は、まだクラス c に対応する型が完全には定義されていないので、 その部分型もはっきりとは分らないという点にあります。 この場合、型変換 (_ :> c)(_ : #c :> c) は 恒等関数と見なされます。
#function x -> (x :> 'a);;
- : 'a -> 'a = <fun>
この結果、次にあげる例のようにもし型変換が self に適用されれば、 self の型は閉じた型になります。 (オブジェクトの型が閉じているとは、 ... が付かないということです。) これはself の型を閉じた型に制限するので、コンパイラに拒否されます。 というのは、self が閉じた型になってしまうと、 クラスを拡張することができなくなってしまうからです。 ですから、self の型が他の型に単一化されて閉じた型になってしまうと、 型エラーが生じます。
#class c = object method m = 1 end
 and d = object (self)
   inherit c
   method n = 2
   method as_c = (self :> c)
 end;;
This expression cannot be coerced to type c = < m : int >; it has type
  < as_c : 'a; m : int; n : int; .. >
but is here used with type c = < m : int >
Self type cannot be unified with a closed object type
しかし、もっともよくあるケースである self を現在のクラスに変換する場合は、型チェッカによって検出され、 適切に型が与えられます。
#class c = object (self) method m = (self :> c) end;;
class c : object method m : c end
これによって、 あるクラスやそのサブクラスのオブジェクト全体のリストを保持するリストが定義できます。
#let all_c = ref [];;
val all_c : '_a list ref = {contents = []}
 
#class c (m : int) =
   object (self)
     method m = m
     initializer all_c := (self :> c) :: !all_c
   end;;
class c : int -> object method m : int end
これを用いると、 型が上位型に変換されたオブジェクトをもとの型に戻すことができます。
#let rec lookup_obj obj = function [] -> raise Not_found
   | obj' :: l ->
      if (obj :> < >) = (obj' :> < >) then obj' else lookup_obj obj l ;;
val lookup_obj : < .. > -> (< .. > as 'a) list -> 'a = <fun>
 
#let lookup_c obj = lookup_obj obj !all_c;;
val lookup_c : < .. > -> < m : int > = <fun>
参照を用いたおかげで、ここで現れている型 < m : int > は単に c を展開したものになります。 オブジェクトをもとの型 c に戻すことができました。


以前に述べた型変換の問題は、 最初にクラス型によって型の略記を定義することで解決できることがあります。
#class type c' = object method m : int end;;
class type c' = object method m : int end
 
#class c : c' = object method m = 1 end
 and d = object (self)
   inherit c
   method n = 2
   method as_c = (self :> c')
 end;;
class c : c'
class d : object method as_c : c' method m : int method n : int end
また、仮想クラスを用いることもできます。 このクラスから継承することによって、c のメソッドが c' を同じ型を持つことを保障することができます。
#class virtual c' = object method virtual m : int end;;
class virtual c' : object method virtual m : int end
 
#class c = object (self) inherit c' method m = 1 end;;
class c : object method m : int end
型の略記を直接定義することを考えるかも知れません。
#type c' = <m : int>;;
しかし、#c' という略記はこのやり型では定義できず、 クラスまたはクラス型定義を通じてのみ定義できます。 というのは、# 略記は暗黙の内に名前のない型変数 .. を持っていて、この型変数を明示的に指定することができないからです。 もっとも近いのは、未定の型を表す新たな型変数を導入して次のようにすることです。
#type 'a c'_class = 'a constraint 'a = < m : int; .. >;;


3.12 Functional objects

インスタンス変数への代入を伴わない point クラスを定義することができます。 {< ... >} 構文は「自分自身」のコピー (つまり現在のオブジェクト) を返します。 この際、インスタンス変数の値を変更することもできます。
#class functional_point y =
   object 
     val x = y
     method get_x = x
     method move d = {< x = x + d >}
   end;;
class functional_point :
  int ->
  object ('a) val x : int method get_x : int method move : int -> 'a end
 
#let p = new functional_point 7;;
val p : functional_point = <obj>
 
#p#get_x;;
- : int = 7
 
#(p#move 3)#get_x;;
- : int = 10
 
#p#get_x;;
- : int = 7
functional_point は再帰的な型であることに注意して下さい。 このことは functional_point のクラス型に現れています。 自分自身の型は 'a'amove メソッドの型に現れています。

上の functional_point の定義は次と同じではありません。
#class bad_functional_point y =
   object 
     val x = y
     method get_x = x
     method move d = new functional_point (x+d)
   end;;
class bad_functional_point :
  int ->
  object
    val x : int
    method get_x : int
    method move : int -> functional_point
  end
 
#let p = new functional_point 7;;
val p : functional_point = <obj>
 
#p#get_x;;
- : int = 7
 
#(p#move 3)#get_x;;
- : int = 10
 
#p#get_x;;
- : int = 7
どちらのクラスに属するオブジェクトも同じ振舞をしますが、 サブクラスのオブジェクトは違った振舞をします。 後者のサブクラスでは、 move メソッドは親クラスのオブジェクトを返します。 反対に、前者のサブクラスでは、move メソッドはサブクラスのオブジェクトを返します。

ここで紹介した構文は、 5.2.1 節で紹介するようにバイナリー・メソッドでよく用いられます。

3.13 Cloning objects

関数型のオブジェクトも手続き型のオブジェクトも、 どちらも複製することができます。 ライブラリ関数の Oo.copy はオブジェクトの「浅い」コピーを作ります。 つまり、もとのオブジェクトと同じ内容のオブジェクトを生成します。 インスタンス変数はコピーされますが、その内容は共有されます。 複製されたオブジェクトのインスタンス変数を (メソッド呼び出しを通じて) 変更しても、もとのオブジェクトのインスタンス変数は変化しません。 同じことは逆の場合 (もとのオブジェクトのインスタンス変数を変更する。) にも成り立ちます。 もちろん、インスタンス変数の内容に代入を行った場合、 (例えば、インスタンス変数の値が参照である場合など) これはもとのオブジェクトと複製の両方に影響します。

Oo.copy の型は次のようになります。
#Oo.copy;;
- : (< .. > as 'a) -> 'a = <fun>
型に現れる as というキーワードは型変数 'a をオブジェクトの型 < .. > に束縛します。 ですから、Oo.copy は任意のメソッドを持つオブジェクトを引数に取り、 同じ型のオブジェクトを返すことになります。 Oo.copy の型は < .. > -> < .. > ではありません。 なぜなら、 二つの「..」は異なったメソッドの組合わせを表しているからです。 「..」は実は型変数のように振舞います。
#let p = new point 5;;
val p : point = <obj>
 
#let q = Oo.copy p;;
val q : < get_offset : int; get_x : int; move : int -> unit > = <obj>
 
#q#move 7; (p#get_x, q#get_x);;
- : int * int = (5, 12)
もし p のクラスが {< >} を返すよう定義されたメソッド copy を公開メソッドとして持てば、 Oo.copy pp#copy と同じように振舞います。

オブジェクトは汎用の比較関数 =<> によって比較することができます。 二つのオブジェクトが等しいとは、これらが物理的に等しい場合です。 特に、オブジェクトとその複製は等しくありません。
#let q = Oo.copy p;;
val q : < get_offset : int; get_x : int; move : int -> unit > = <obj>
 
#p = q, p = p;;
- : bool * bool = (false, true)
他の汎用の比較関数 (<, <=,...) もオブジェクトに対して用いることができます。 <= の意味は特定されていませんが、 全順序を与えることが保証されています。 オブジェクト間の順序は、オブジェクトの生成時に決定され、 インスタンス変数が変化しても順序が変化することはありません。

Oo.copy{< >} は似た所があります。 どちらも、オブジェクト内で、 インスタンス変数を変えない自分自身のコピーを生成するのに使えます。
#class copy =
   object
     method copy = {< >}
   end;;
class copy : object ('a) method copy : 'a end
 
#class copy =
   object (self)
     method copy = Oo.copy self
   end;;
class copy : object ('a) method copy : 'a end
しかし、{< ... >} だけがインスタンス変数を書き換えられます。 また、Oo.copy だけがコピーしたいオブジェクトの外で使えます。

複製はオブジェクトの状態を保存したり、 過去の状態に復帰させたりすることにも使えます。
#class backup = 
   object (self : 'mytype)
     val mutable copy = None
     method save = copy <- Some {< copy = None >}
     method restore = match copy with Some x -> x | None -> self
   end;;
class backup :
  object ('a)
    val mutable copy : 'a option
    method restore : 'a
    method save : unit
  end
上の例では、バックアップを一つだけ作ります。 多重継承を用いて、どんなクラスにもバックアップ機能を付け加えることができます。
#class ['a] backup_ref x = object inherit ['a] ref x inherit backup end;;
class ['a] backup_ref :
  'a ->
  object ('b)
    val mutable copy : 'b option
    val mutable x : 'a
    method get : 'a
    method restore : 'b
    method save : unit
    method set : 'a -> unit
  end
 
#let rec get p n = if n = 0 then p # get else get (p # restore) (n-1);;
val get : (< get : 'b; restore : 'a; .. > as 'a) -> int -> 'b = <fun>
 
#let p = new backup_ref 0  in
 p # save; p # set 1; p # save; p # set 2; 
 [get p 0; get p 1; get p 2; get p 3; get p 4];;
- : int list = [2; 1; 1; 1; 1]
すべてのコピーを保持するようなバックアップ機能を考えることもできます。 (すべてのバックアップを除去するメソッドを付け加えました。)
#class backup = 
   object (self : 'mytype)
     val mutable copy = None
     method save = copy <- Some {< >}
     method restore = match copy with Some x -> x | None -> self
     method clear = copy <- None
   end;;
class backup :
  object ('a)
    val mutable copy : 'a option
    method clear : unit
    method restore : 'a
    method save : unit
  end
#class ['a] backup_ref x = object inherit ['a] ref x inherit backup end;;
class ['a] backup_ref :
  'a ->
  object ('b)
    val mutable copy : 'b option
    val mutable x : 'a
    method clear : unit
    method get : 'a
    method restore : 'b
    method save : unit
    method set : 'a -> unit
  end
 
#let p = new backup_ref 0  in
 p # save; p # set 1; p # save; p # set 2; 
 [get p 0; get p 1; get p 2; get p 3; get p 4];;
- : int list = [2; 1; 0; 0; 0]
3.14 Recursive classes

再帰的なクラスを、型が相互に再帰的なオブジェクトを定義するのに使えます。
#class window =
   object 
     val mutable top_widget = (None : widget option)
     method top_widget = top_widget
   end
 and widget (w : window) =
   object
     val window = w
     method window = window
   end;;
class window :
  object
    val mutable top_widget : widget option
    method top_widget : widget option
  end
class widget :
  window -> object val window : window method window : window end
型は相互に再帰的ですが、widget クラスと window クラス自体は独立です。

3.15 Binary methods

バイナリー・メソッドとは、 そのオブジェクト自体と同じ型の引数を取るメソッドのことです。 下の comparable クラスは型 'a -> bool のバイナリー・メソッド leq を持つクラスのテンプレートです。 ここで 'a はオブジェクト自体の型に束縛されています。 ですから、#comparable< leq : 'a -> bool; .. > as 'a の略記となります。 ここで as が再帰的な型を表記するのに使えることが分ります。
#class virtual comparable = 
   object (_ : 'a)
     method virtual leq : 'a -> bool
   end;;
class virtual comparable : object ('a) method virtual leq : 'a -> bool end
さて、comparable のサブクラス money を定義しましょう。 money クラスは、単に浮動小数点型を comparable オブジェクトになるようにしたものです。 後で、もっと操作を加えることにします。 <= は Objective Caml では多相型を持つので、 クラス引数には型の制限が与えられています。 inherit 節によってこのクラスのオブジェクトが #comparable に合致することが保証されています。
#class money (x : float) =
   object
     inherit comparable
     val repr = x
     method value = repr
     method leq p = repr <= p#value
   end;;
class money :
  float ->
  object ('a)
    val repr : float
    method leq : 'a -> bool
    method value : float
  end
money1comparable の部分型でないことに注意して下さい。 というのは、自分自身の型が引数の位置に現れているからです。 実際、クラス money のオブジェクト m のメソッド leq は引数の value メソッドを呼び出します。 もし、m が型 comparable を持つとみなせたとすると、 value メソッドを持たないオブジェクトを引数として mleq メソッドを呼び出せることになり、エラーとなります。 同じように、次の型 money2money の部分型ではありません。
#class money2 x =
   object   
     inherit money x
     method times k = {< repr = k *. repr >}
   end;;
class money2 :
  float ->
  object ('a)
    val repr : float
    method leq : 'a -> bool
    method times : float -> 'a
    method value : float
  end
しかし、moneymoney2 のいずれの型を持つオブジェクトでも機能する関数を定義することができます。 関数 min#comparable に合致する型を持つ二つのオブジェクトのうち、 最小のものを返します。 min の型は #comparable -> #comparable -> #comparable ではありません。 というのは、#comparable は型変数 (.. の部分) を隠すからです。 それぞれの #comparable が新しい型変数を導入してしまいます。
#let min (x : #comparable) y =
   if x#leq y then x else y;;
val min : (#comparable as 'a) -> 'a -> 'a = <fun>
この関数は moneymoney2 のどちらの型をもつオブジェクトにも適用できます。
#(min (new money  1.3) (new money 3.1))#value;;
- : float = 1.3
 
#(min (new money2 5.0) (new money2 3.14))#value;;
- : float = 3.14
バイナリー・メソッドの他の例が 5.2.1 節と 5.2.3 にあげられています。

times メソッドで {< ... >} 構文を用いていることに注意して下さい。 {< repr = k *. repr >} の代わりに new money2 (k *. repr) と書くと、継承の際に問題を起こします。 money2 のサブクラス money3times メソッドが期待した money3 クラスではなく money2 クラスのオブジェクトを返すようになってしまいます。

money クラスは他のバイナリー・メソッドを持つことがありえますし、 自然でもあります。
#class money x =
   object (self : 'a)
     val repr = x
     method value = repr
     method print = print_float repr
     method times k = {< repr = k *. x >}
     method leq (p : 'a) = repr <= p#value
     method plus (p : 'a) = {< repr = x +. p#value >}
   end;;
class money :
  float ->
  object ('a)
    val repr : float
    method leq : 'a -> bool
    method plus : 'a -> 'a
    method print : unit
    method times : float -> 'a
    method value : float
  end
3.16 Friends

上記の money クラスは、 バイナリー・メソッドとともによく生じる問題を示しています。 同じクラスの他のオブジェクトとやりとりするために、 money オブジェクトの内部表現を value のようなメソッドを通じて公開しなくてはいけません。 もしすべてのバイナリー・メソッド (ここでは plusleq) を取り除けば、 内部表現は value を取り除くことで隠蔽することができます。 しかし、このことはバイナリー・メソッドが同じクラスに属するが、 自分自身とは異なるオブジェクトの内部表現を利用する限り不可能です。
#class safe_money x =
   object (self : 'a)
     val repr = x
     method print = print_float repr
     method times k = {< repr = k *. x >}
   end;;
class safe_money :
  float ->
  object ('a)
    val repr : float
    method print : unit
    method times : float -> 'a
  end
ここでは、オブジェクトの内部表現は個々のオブジェクトのみに知られています。 内部表現を同じクラスの他のオブジェクトに公開しようとすると、 これを全世界に公開することを強いられてしまうのです。 しかし、内部表現の可視性はモジュールによって簡単に制限できます。
#module type MONEY = 
   sig 
     type t
     class c : float -> 
       object ('a)
         val repr : t
         method value : t
         method print : unit
         method times : float -> 'a
         method leq : 'a -> bool
         method plus : 'a -> 'a 
       end
   end;;
 
 module Euro : MONEY = 
   struct
     type t = float
     class c x =
       object (self : 'a)
         val repr = x
         method value = repr
         method print = print_float repr
         method times k = {< repr = k *. x >}
         method leq (p : 'a) = repr <= p#value
         method plus (p : 'a) = {< repr = x +. p#value >}
       end
   end;;
フレンドの他の例は 5.2.3 節で見ることができます。 これらの例は、オブジェクト (ここでは同じクラスに属するオブジェクト) と関数のある集まり (フレンド) が互いに相手の内部表現を知る必要があるのに、 その外側からは内部表現が隠されている必要があるときに生じます。 この問題を解決するには、すべてのフレンドをかならず同じモジュールで定義して、 内部表現を得る方法を与え、 かつその表現を抽象型にしてモジュールの外から隠してしまうことです。




Previous Contents Next