3.12 型変換の使用

部分型関係が暗黙に導入されることは決してありません。部分型を導入する方法はふたつあります。もっとも汎用的な構文では、型変換の定義域と余定義域(訳注: 変換元と変換後の型)を完全に明示します。

以前見たように、「点」と「色付きの点」は互換性のない型でした。例えば、この二つを同じリストの要素にすることはできません。しかし、「色付きの点」の 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 >
    

実際、実行時の検査なしに型を狭い方向へ変換するのは安全ではないでしょう。実行時型検査で例外の発生する可能性もありますが、そのためには実行時にも型情報が必要になります。しかし、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>x
    

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 の定義で陽に再帰したことにより)型 #c2 = < m : 'a; .. > as 'a は多相再帰的な型になっています。このため、c0 クラスのオブジェクトにも適応できます。一方、最初の例では、c1 は2回展開され、型 < m : < m : c1; .. >; .. > になり、再帰的な型にはなりません(#c1 = < m : c1; .. > です)。また、 to_c2 の型は #c2 -> c2 であるのに対し、 to_c1 の型が #c1 -> c1 よりも一般的であることに気づかれたかもしれません。しかし、 3.16 節「バイナリメソッド」で説明するように、型 #c のインスタンスが c の子クラスにならないこともあるため、この型の方が常により一般的であると言えるわけではありません。それでも、パラメータ付きクラスでなければ、 (_ :> c)(_ : #c :> c) より常に一般的です。

クラス c の定義中にクラス c への変換を定義しようとするとよく知られた問題に行き当たります。問題は、まだクラス c に対する型略記が完全には定義されていないので、その部分型もはっきりとは分らないという点にあります。この場合、型変換 (_ :> c)(_ : #c :> c) は次のように恒等関数と見なされます。

#function x -> (x :> 'a);;
- : 'a -> 'a = <fun>
    

結果として、次のように、この型変換が self に適用されると、 self の型は閉じた型 c になります(.. の付かないオブジェクト型を閉じたオブジェクト型と言います)。これは 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
    

これを用いると、型を c に弱めた状態のオブジェクトを探索することができます。

#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 endx
    

また、抽象クラスを使うこともできます。このクラスから継承することによって、 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; .. >;;