M.Hiroi's Home Page

お気楽 OCaml プログラミング入門

クラス型


Copyright (C) 2008-2020 Makoto Hiroi
All rights reserved.

はじめに

前回は多重継承と 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.

初版 2008 年 7 月 27 日
改訂 2020 年 7 月 19 日