Chapter 3 Camlのオブジェクトこのページは最後に更新されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。 last mod. 2008-06-03 (火) 15:03:56
The Objective Caml system release 3.10 (Chapter written by Jerome Vouillon, Didier Remy and Jacques Garrigue) この章では、Objective Camlのオブジェクト指向機能について概観します。 注意: Objetive Camlにおけるオブジェクトとクラス、型の関係は、JavaやC++などの主要なオブジェクト指向言語のものと大きく異なっています。ですので、似た用語であっても異なるものを指すことがあります。 クラスとオブジェクト下に示されたクラス pointは、ひとつのインスタンス変数xと二つのメソッドget_x、move を定義しています。インスタンス変数 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 クラス point は x の初期値を引数として取るよう定義することもできます。 #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.4 節で述べる initializer を用いる方法があります。 即値オブジェクトクラスを作らずに、直接的にオブジェクトを生成する方法もあります。 文法はクラス定義とまったく同じですが、結果はクラスではなくオブジェクトになります。このあとの節で述べることは、この即値オブジェクトにも適用することができます。 #let p = # object # val mutable x = 0 # method get_x = x # method move d = x <- x + d # end;; val p : < get_x : int; move : int -> unit > = <obj> #p#get_x;; - : int = 0 #p#move 3;; - : unit = () #p#get_x;; - : int = 3 クラスは式中で定義することはできませんが、即値オブジェクトはどこでも用いることができます。さらに、その環境で定義された変数も参照できます。 #let minmax x y = # if x < y then object method min = x method max = y end # else object method min = y method max = x end;; val minmax : 'a -> 'a -> < max : 'a; min : 'a > = <fun> 即値オブジェクトはクラスと比べると2つの欠点があります。 型が略記されないことと、継承できないことです。 節3.3や3.10で説明するように、この欠点が有用であることもあります。 自分自身への参照メソッドや 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 はそのサブクラスのオブジェクトに束縛されます。 selfに関するよくある問題は、それがサブクラスで拡張可能であるという問題点です。これは避けることができません。簡単な例は以下のようになります。 #let ints = ref [];; val ints : '_a list ref = {contents = []} #class my_int = # object (self) # method n = 1 # method register = ints := self :: !ints # end;; This expression has type < n : int; register : 'a; .. > but is here used with type 'b Self type cannot escape its class 最初の2つのエラーは無視することができます。最後のエラーで問題になっているのは、selfを外部の参照に置くことで、あとでそれが拡張できなくなるという点です。これについては、3.12で詳しく説明します。さしあたっては、即値オブジェクトは拡張ができないので、この問題は起こらないということを覚えておいてください。 #let my_int = # object (self) # method n = 1 # method register = ints := self :: !ints # end;; val my_int : < n : int; register : unit > = <obj> 初期化クラス定義内の 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 節に挙げられています。 仮想メソッドvirtual というキーワードを用いることで、メソッドを宣言だけして定義しないですますことができます。このようなメソッドのことを仮想メソッドと呼びます。仮想メソッドの定義は後にサブクラスで与えることができます。仮想メソッドを含むクラスを仮想クラスと呼びます。仮想クラスを定義する際には、クラス定義にも virtual キーワードが必要です。また、仮想クラスのインスタンス、つまりこのクラスのオブジェクトを生成することはできません。一方、仮想クラスの定義は、通常のクラスと同じく型名を定義します。仮想クラスに対応する型では仮想メソッドは、通常のメソッドと同じように扱われます。 #class virtual abstract_point x_init = # object (self) # 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 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 # 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_offset : int method get_x : int method move : int -> unit end インスタンス変数もvirtualと宣言することで、メソッドの場合と同じ効果が得られます。 #class virtual abstract_point2 = # object # val mutable virtual x : int # method move d = x <- x + d # end;; class virtual abstract_point2 : object val mutable virtual x : int method move : int -> unit end #class point2 x_init = # object # inherit abstract_point2 # val mutable x = x_init # method get_offset = x - x_init # end;; class point2 : int -> object val mutable x : int method get_offset : int method move : int -> unit end プライベート・メソッドプライベート・メソッドとはオブジェクトのインターフェースに現れないメソッドです。プライベート・メソッドは同じオブジェクトの他のメソッドからのみ呼び出すことができます。 #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 = () これはJavaやC++のプライベート・メソッドやプロテクテッド・メソッドとは異なることに注意してください。JavaやC++では同じクラスの別のオブジェクトから呼べますが、Objective Camlでは呼べません。これは、Objective Camlでは型とクラスが独立していることによるものです。無関係のクラスが同じ型のオブジェクトを生成することができるので、型レベルでオブジェクトが同じクラスから生成されたか確かめることができません。しかし、3.17節で述べるようにfriendメソッドを実現することはできます。 後で述べるように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 の順で与えられなければなりません。 クラス・インタフェースクラス・インターフェースはクラス定義から自動的に導かれますが、直接定義してクラスの型を制限するのに用いることができます。クラス定義と同じく、クラス・インターフェースの定義も新しい型の略記を定義します。 #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 もしくは #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 継承継承を説明するため、「点」のクラスから継承によって定義された、「色付きの点」のクラスを定義してみましょう。このクラスは 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 に適用することができます。 また p は get 以外のメソッドを持つこともできます。これは 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> 多重継承多重継承をすることもできます。同じメソッドの定義の内、最後のものだけが有効です。もしサブクラスで参照可能な親クラスのメソッドがサブクラスで再び定義された場合、メソッドはサブクラスのものに上書きされます。しかし、親クラスの定義も、親クラスを識別子に束縛することにより用いることができます。次の例では、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 は、それが導入された順に評価されます。 パラメータ化されたクラス参照セルはオブジェクトとして実装できます。しかし、素朴な定義は型チェックを通過できません。 #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 即値オブジェクトはクラスを定義しないので、この制限はありません。 #let new_ref x_init = # object # val mutable x = x_init # method get = x # method set y = x <- y # end;; val new_ref : 'a -> < get : 'a; set : 'a -> unit > = <fun> 一方、多相型の参照を持つクラス定義をあたえるには、その型変数を明示的に与える必要があります。クラスの型変数はかならず [ と ] に囲まれた部分に列挙されなくてはいけません。多相型を持つオブジェクトの型は、この型変数を使って制限されなければいけません。 #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 クラスの定義を得ることができます。このクラス定義は、引数に対して若干以前よりも強い制限を科します。というのは center が get_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 多相メソッド上記のように、パラメータ付きクラスは異なった型の値をその内容として保持するインスタンスを持つことができました。しかし、これはメソッドを多相型にするには不十分です。 典型的な例は反復子です。 #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 " ここでコンパイラが生成したクラス型を見て分かるように、クラス定義では多相型メソッドの型変数は明示的に量化*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 は同じ型を持たなくてはならず、その型変数 'b は同じ量化子で束縛されています。 型変換の使用型変換はかならず明示的に行われます。変換を行う方法は二つあります。もっとも一般的な方法では、変換前の型と変換後の型を共に明示的に指定します。 以前見たように、「点」と「色付きの点」は異なった型を持っていました。例えば、この二つを同じリストの要素にすることはできません。しかし、「色付きの点」の 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' に変換できるのは、 t が t' の部分型であるときだけです。例えば、「点」を「色付きの点」に変換することはできません。 #(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 > 実際、部分型への変換は安全ではないかもしれません。実行時型検査が例外を投げるためには、実行時にも型情報が必要になります。しかし、Objective Camlは型情報を持たないので、部分型への変換を行えません。 部分型と継承とは何の関係もないことに注意して下さい。継承はあるクラスを別のクラスから生成するための構文上の機構です。一方、部分型は型の間の意味上の関連です。例えば、「色付きの点」のクラスを「点」からの継承を用いずに、直接定義することもできます。この場合でも「色付きの点」の型は変化せず、よって「点」の部分型になります。 型変換を行う場合、型変換前の型は省略することができます。例えば、次のような定義が可能です。 #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 型 c0 は <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 の略記になります。型 c0 から 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 > but is here used with type < m : #c1 as 'a; .. > Type c0 = < m : c0; n : int > is not compatible with type 'a = < m : c1; .. > Type c0 = < m : c0; n : int > is not compatible with type c1 = < m : c1 > The second object type has no 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> c1 と c2 のクラス型はことなっていますが、 c1 と c2 は同じ型を定義します。というのは、同じ型の同じメソッドを持つからです。しかし、もし型変換前の型が省略され、型変換後の型が知られているクラス型から導かれている時、型ではなくクラス型が型変換関数を導くのに用いられます。このことによって、部分型から上位型への変換の際には、ほとんどの場合、型変換前の型は明示的に指定する必要がありません。型変換関数の型は次のようになります。 #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.16節で説明するように型 #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 : c; m : int; n : int; .. > but is here used with type c 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' and 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; .. >;; 関数オブジェクトインスタンス変数への代入を伴わない 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 で 'a が move メソッドの型に現れています。 上の functional_point の定義は次と同じではありません。 #class bad_functional_point y = # object # val x = y # method get_x = x # method move d = new bad_functional_point (x+d) # end;; class bad_functional_point : int -> object val x : int method get_x : int method move : int -> bad_functional_point end どちらのクラスに属するオブジェクトも同じ振舞をしますが、サブクラスのオブジェクトは違った振舞をします。後者のサブクラスでは、 move メソッドは親クラスのオブジェクトを返します。反対に、前者のサブクラスでは、move メソッドはサブクラスのオブジェクトを返します。 ここで紹介した関数的アップデートの構文は、 5.2.1 節で紹介するようにバイナリー・メソッドでよく用いられます。 オブジェクトの複製関数型のオブジェクトも手続き型のオブジェクトも、どちらも複製することができます。ライブラリ関数の 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 : point = <obj> #q#move 7; (p#get_x, q#get_x);; - : int * int = (5, 12) もし p のクラスが {< >} を返すよう定義されたメソッド copy を公開メソッドとして持てば、 Oo.copy p は p#copy と同じように振舞います。 オブジェクトは汎用の比較関数 = や <> によって比較することができます。二つのオブジェクトが等しいとは、これらが物理的に等しい場合です。特に、オブジェクトとその複製は等しくありません。 #let q = Oo.copy p;; val q : point = <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] 再帰クラス再帰的なクラスを、型が相互に再帰的なオブジェクトを定義するのに使えます。 #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 and widget : window -> object val window : window method window : window end 型は相互に再帰的ですが、widget クラスと window クラス自体は独立です。 バイナリー・メソッドバイナリー・メソッドとは、そのオブジェクト自体と同じ型の引数を取るメソッドのことです。下の 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 型 money1 は comparable の部分型でないことに注意して下さい。というのは、自分自身の型が引数の位置に現れているからです。実際、クラス money のオブジェクト m のメソッド leq は引数の value メソッドを呼び出します。もし、m が型 comparable を持つとみなせたとすると、 value メソッドを持たないオブジェクトを引数として m の leq メソッドを呼び出せることになり、エラーとなります。 同じように、次の型 money2 は money の部分型ではありません。 #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 しかし、money と money2 のいずれの型を持つオブジェクトでも機能する関数を定義することができます。関数 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> この関数は money と money2 のどちらの型をもつオブジェクトにも適用できます。 #(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 のサブクラス money3 で times メソッドが期待した 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 Friend上記の money クラスは、バイナリー・メソッドとともによく生じる問題を示しています。同じクラスの他のオブジェクトとやりとりするために、 money オブジェクトの内部表現を value のようなメソッドを通じて公開しなくてはいけません。もしすべてのバイナリー・メソッド (ここでは plus と leq) を取り除けば、内部表現は 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 節で見ることができます。これらの例は、オブジェクト (ここでは同じクラスに属するオブジェクト) と関数のある集まり (フレンド) が互いに相手の内部表現を知る必要があるのに、その外側からは内部表現が隠されている必要があるときに生じます。この問題を解決するには、すべてのフレンドをかならず同じモジュールで定義して、内部表現を得る方法を与え、かつその表現を抽象型にしてモジュールの外から隠してしまうことです。 |