オブジェクト
オブジェクトとクラス
OCamlはオブジェクト指向、命令型、関数型言語だ :-)。 これらのパラダイムを混ぜて、 手元の仕事にもっともふさわしい(あるいは慣れた) プログラミングパラダイムが使える。 本章では OCaml でのオブジェクト指向プログラミングに注目するが、 何故オブジェクト指向のプログラムを書きたいか、 あるいは書きたくないかについても話題にする。
オブジェクト指向プログラミングを示すのに、 教科書で用いられる古典的でばかげた例として、 スタッククラスがある。 これはいろんな意味でとてもひどい例だが、 オブジェクト指向 OCaml を書く基本をこれで紹介する。
整数のスタックを提供する基本コードがこれだ。 このクラスは線形リストで実装してある。
# class stack_of_ints =
object (self)
val mutable the_list = ( [] : int list ) (* インスタンス変数 *)
method push x = (* push メソッド *)
the_list <- x :: the_list
method pop = (* pop メソッド *)
let result = List.hd the_list in
the_list <- List.tl the_list;
result
method peek = (* peek メソッド *)
List.hd the_list
method size = (* size メソッド *)
List.length the_list
end;;
class stack_of_ints :
object
val mutable the_list : int list
method peek : int
method pop : int
method push : int -> unit
method size : int
end
class name = object (self) ... end
が name
という
名前のクラスを定義する基本パターンだ。
クラスには the_list
という名前の変更可能なインスタンス変数(定数ではない)
がひとつある。これは基礎的な線形リストだ。
ちょっと分かりにくいかもしれないがちょっとしたコードを使って、
この変数を初期化(stack_of_ints
のオブジェクトが生成されるたびに)する。
この ( [] : int list )
式は「int list
型の空リスト」という意味だ。
単純な空リスト []
は 'a list
型、
つまり任意型のリストであったことを思いだそう。 欲しいのは int
のスタックであって他の何者でもないのだから、
ここでは、型推論エンジンに対して一般的な「任意のリスト」ではなく
実際にはもっと狭い「int
のリスト」ということを伝えている。
( 式 : 型 )
という文法は、 その式
がその型
であることを意味する。
これは一般的な型キャストではない。というのも、
型推論エンジンを無効にするのではなく、
汎用の型をもっと特定のものに狭めるだけだからだ。 だから例えば
( 1 : float )
とは書けない。
# (1 : float);;
Error: This expression has type int but an expression was expected of type
float
型安全は保存される。じゃあ例に戻ろうか...
このクラスには4つの単純なメソッドがある。 push
は整数をスタックにプッシュする(積む)。 pop
はスタックの先頭の整数をポップ(取り出し)してそれを返す。 <-
(割り当て演算子)は変更可能なインスタンス変数の更新があるときに用いる。
同じく <-
割り当て演算子でレコード内の破壊可能フィールドの更新にも用いる。
peek
はスタックに影響を及ぼさずに先頭(つまりリストの先頭)を返し、
size
はスタックの要素数(つまりリストの長さ)を返す。
この整数スタックをテストするコードを書いてみよう。
まず新しいオブジェクトを生成しよう。 new
演算子というお馴染のものが使える。
# let s = new stack_of_ints;;
val s : stack_of_ints = <obj>
ではいくつかの要素をスタックにプッシュしてポップしよう。
# for i = 1 to 10 do
s#push i
done;;
- : unit = ()
# while s#size > 0 do
Printf.printf "Popped %d off the stack.\n" s#pop
done;;
Popped 10 off the stack.
Popped 9 off the stack.
Popped 8 off the stack.
Popped 7 off the stack.
Popped 6 off the stack.
Popped 5 off the stack.
Popped 4 off the stack.
Popped 3 off the stack.
Popped 2 off the stack.
Popped 1 off the stack.
- : unit = ()
文法に注意しよう。 object#method
は object
の method
を呼び出すという意味だ。 これは他の命令型言語でいうところの
object.method
やら object->method
と同じことだ。
OCaml のトップレベルだとオブジェクトやメソッドの型が詳細に分かる。
# let s = new stack_of_ints;;
val s : stack_of_ints = <obj>
# s#push;;
- : int -> unit = <fun>
s
は不明確なオブジェクトだ。
実装(つまりリスト)は呼び出し側から隠されている。
多相クラス
整数のスタックはこれでいいが、どんな型でも格納できるスタックはどうだろう?
(混合型を格納できる1つのスタックでなく、任意の一つの型のオブジェクトを
格納できる複数のスタックだ)。 'a list
という感じで 'a stack
を定義しよう。
# class ['a] stack =
object (self)
val mutable list = ( [] : 'a list ) (* instance variable *)
method push x = (* push method *)
list <- x :: list
method pop = (* pop method *)
let result = List.hd list in
list <- List.tl list;
result
method peek = (* peek method *)
List.hd list
method size = (* size method *)
List.length list
end;;
class ['a] stack :
object
val mutable list : 'a list
method peek : 'a
method pop : 'a
method push : 'a -> unit
method size : int
end
class ['a] stack
は実際には一つのクラスを定義しているのではなく、
可能な型について全部の「クラスのクラス」を定義している
(つまり無限に多くのクラスだ!)。 じゃぁ 'a stack
クラスを使ってみよう。
この例ではスタックを生成して浮動小数点数をこのスタックにプッシュする。
スタックの型に注意しよう。
# let s = new stack;;
val s : '_weak1 stack = <obj>
# s#push 1.0;;
- : unit = ()
# s;;
- : float stack = <obj>
このスタックは今 float stack
になっており、
このスタックでは浮動小数点数しかプッシュ、ポップできないようだ ( '_a
記法の説明に付いては OCaml expert
FAQ
を見よ)。 この新しい float stack
の型安全を実証しよう。
# s#push 3.0;;
- : unit = ()
# s#pop;;
- : float = 3.
# s#pop;;
- : float = 1.
# s#push "a string";;
Error: This expression has type string but an expression was expected of type
float
どんな型のスタックも操作できる多相関数を定義できる。 最初にこれを試してみよう。
# let drain_stack s =
while s#size > 0 do
ignore (s#pop)
done;;
val drain_stack : < pop : 'a; size : int; .. > -> unit = <fun>
drain_stack
の型に注意だ。 賢いことに、たぶん賢すぎることに、 OCaml
の型推論エンジンは pop
, size
メソッドを持つ
任意のオブジェクトで動作する drain_stack
を推論した! だから、もし、
適切な型署名のある pop
, size
メソッドを含んでいる全く別のクラスを定義していたら、
それとは異なる型のオブジェクトで偶然 drain_stack
を呼び出してしまうかもしれない。
OCaml にもっと特定させるよう強制でき、 以下のように drain_stack
が
引数 s
の型を制約し 'a stack
(訳注 stack
クラスに属する任意型)
で呼び出すことのみ許容するようにできる。
# let drain_stack (s : 'a stack) =
while s#size > 0 do
ignore (s#pop)
done;;
val drain_stack : 'a stack -> unit = <fun>
継承、仮想クラス、初期化子
私は Java プログラマが継承を過剰に使う傾向にあることに気づいた。 おそらく Java では継承がコードを拡張する唯一の合理的な方法だからだ。 コードを拡張するより良い、より一般的な方法は、普通はフックを使うことだ (cf. Apache のモジュール API)。 にもかかわらず、特定の狭い領域では継承は役に立つし、 GUI ウィジェットライブラリを書くときはもっとも重要だ。
架空の OCaml のウィジェットライブラリを Java の Swing と 同じようなものと考えよう。 ボタンとラベルを次のクラス階層で定義する:
widget (superclass for all widgets)
|
+----> container (any widget that can contain other widgets)
| |
| +----> button
|
+-------------> label
(button
は container
であることに注意。
なぜならラベルかイメージのいずれかを含むことが出来て、
それはボタンに何が表示されるかに依存するからだ。)
widget
はウィジェット全てに対する仮想スーパークラスである。
ウィジェットそれぞれに、生存期間中一定した名前(文字列)が欲しい。
最初の試みはこれだ:
# class virtual widget name =
object (self)
method get_name =
name
method virtual repaint : unit
end;;
Error: Some type variables are unbound in this type:
class virtual widget :
'a ->
object method get_name : 'a method virtual repaint : unit end
The method get_name has type 'a where 'a is unbound
おぉっと! OCaml は name
の型を推論できずに 'a
と推定することを忘れていたよ。 しかしこれは多相クラスを定義しており、
クラスを多相として宣言(class ['a] widget
)しなかった。 name
の型を狭める必要がある。このように:
# class virtual widget (name : string) =
object (self)
method get_name =
name
method virtual repaint : unit
end;;
class virtual widget :
string -> object method get_name : string method virtual repaint : unit end
このコードには幾つかあたらしいことがある。
まず、このクラスには初期化子が含まれている。
ちょうど例えばJavaでのコンストラクタの引数と同じものと考えられる
クラス(name
)への引数だ。
public class Widget
{
public Widget (String name)
{
...
}
}
OCaml ではコンストラクタはクラス全体を構築し、 単なる特定の名前つき関数ではないから、 クラスに引数があるかのように引数を書く。
class foo arg1 arg2 ... =
二つ目に、このクラスは仮想メソッドを含んでおり、
その結果クラス全体も仮想とマークされている。 仮想メソッドはここでは
repaint
である。 OCaml にそれが仮想(method virtual
)
であることを教える必要があり、 さらに OCaml
にこのメソッドの型を教える必要がある。
なぜならこのメソッドにはコードが全く含まれず、 OCaml
は自動で型推論をして型をあたえることは出来ないので、
型を教えてやらないといけない。 この場合、メソッドは単に unit
を返す。
もしクラスに仮想メソッド(単に継承されただけのものでも)を含む場合、
クラス全体を仮想として class virtual ...
と指定しなければならない。
C++ や Java 同様、仮想クラスは直接 new
でインスタンス化出来ない:
# let w = new widget "my widget";;
Error: Cannot instantiate the virtual class widget
この container
クラスはもっと面白くなる。 widget
から継承されなければならず、
ウィジェットを含むリストを格納する仕掛けを持たなければならない。
次に単純な container
の実装を示す;
# class virtual container name =
object (self)
inherit widget name
val mutable widgets = ( [] : widget list )
method add w =
widgets <- w :: widgets
method get_widgets =
widgets
method repaint =
List.iter (fun w -> w#repaint) widgets
end;;
class virtual container :
string ->
object
val mutable widgets : widget list
method add : widget -> unit
method get_name : string
method get_widgets : widget list
method repaint : unit
end
注意:
container
クラスは仮想とマークされている。仮想メソッドを全く含んでいないが、この場合、誰かが直接コンテナを生成するのを防ぎたいからだ。container
クラスはwidget
を構築するときに直接渡すname
引数がある。inherit widget name
はcontainer
がwidget
から継承されており、name
引数がwidget
のコンストラクタに渡されることを意味する。container
にはウィジェットの変更可能リストと、ウィジェットをリストに加えるadd
メソッド、ウィジェットのリストを返すget_widgets
メソッドが含まれる。get_widget
で返されるウィジェットのリストはクラスの外部のコードでは変更できない。理由はちょっと微妙だが、基本的に OCaml の線形リストは変更不能であるという事実に落ち着く。誰かが書いた次のコードを想像しよう:let list = container#get_widgets in x :: list
これで、
x
がウィジェットのリストに追加されることによって、container
クラスのプライベートな内部表現が変更されるだろうか? そうはならない。 プライベート変数であるwidgets
は、 これや他のいかなる外部コードによる変更の試みによっても影響されない。 これは例えば、 後日内部表現に配列を使うように変えても外部コードからクラスを変更する必要もないことを意味する。
最後に、ちょっとじゃないけど、 以前の仮想 repaint
関数、すなわち
container#repaint
でウィジェットに含まれる全てを再描画するよう実装する。 List.iter
を用いてリストを全部繰り返すことと、
馴染の薄いであろう匿名関数式も使うことに注意しよう。
# (fun w -> w#repaint);;
- : < repaint : 'a; .. > -> 'a = <fun>
一つの引数 w
を伴う匿名関数を定義して w#repaint
(ウィジェット w
の
repaint
メソッド) を呼び出している。
この例では button
クラスは単純である
(むしろ実際には非現実的な程単純だが気にしない):
type button_state = Released | Pressed;;
# type button_state = Released | Pressed;;
type button_state = Released | Pressed
# class button ?callback name =
object (self)
inherit container name as super
val mutable state = Released
method press =
state <- Pressed;
match callback with
| None -> ()
| Some f -> f ()
method release =
state <- Released
method repaint =
super#repaint;
print_endline ("Button being repainted, state is " ^
(match state with
| Pressed -> "Pressed"
| Released -> "Released"))
end;;
class button :
?callback:(unit -> unit) ->
string ->
object
val mutable state : button_state
val mutable widgets : widget list
method add : widget -> unit
method get_name : string
method get_widgets : widget list
method press : unit
method release : unit
method repaint : unit
end
注意:
- この関数はオプション引数(前の章を参照)があり、オプションのコールバック関数を渡している。コールバックはボタンが押されたときに呼ばれる。
inherit container name as super
式の意味は、スーパークラスにsuper
と命名している。ここでrepaint
メソッドをsuper#repaint
と用いている。これはスーパークラスのメソッド呼び出しを明示している。- ボタンの押下 (このかなり安直なコードでの
button#press
の呼び出し) によってボタンの状態がPressed
に設定され、もし定義されていればコールバック関数が呼び出される。callback
変数はNone
かSome f
のいずれか、言い替えると(unit -> unit) option
型であることに注意。これについて自信がなければ前の章を読み直そう。 - コールバック変数について奇妙なことに注意。これはクラスへの引数として定義されているが、どのメソッドからも見えるし使うことが出来る。言い替えれば、この変数はオブジェクトが生成されたときに供給されるが、オブジェクトの生存期間中ずっと生きつづける。
repaint
メソッドが実装された。これはスーパークラス(コンテナの再描画)を呼び出してからボタンを再描画し、ボタンの現在の状態を表示する。
label
を定義する前に、 OCaml トップレベル環境で button
クラスで遊んでみよう。
# let b = new button ~callback:(fun () -> print_endline "Ouch!") "button";;
val b : button = <obj>
# b#repaint;;
Button being repainted, state is Released
- : unit = ()
# b#press;;
Ouch!
- : unit = ()
# b#repaint;;
Button being repainted, state is Pressed
- : unit = ()
# b#release;;
- : unit = ()
ここに比較的平凡な label
のクラスがある:
# class label name text =
object (self)
inherit widget name
method repaint =
print_endline ("Label: " ^ text)
end;;
class label :
string ->
string -> object method get_name : string method repaint : unit end
「Press me!」というラベルを作ってボタンに加えよう。
# let l = new label "label" "Press me!";;
val l : label = <obj>
# b#add l;;
- : unit = ()
# b#repaint;;
Label: Press me!
Button being repainted, state is Released
- : unit = ()
self
に関する註
上の例は全て汎用パターンを使ってクラスを定義した。
class name =
object (self)
(* ... *)
end
self
への参照について説明していない。
実際にはこれはオブジェクトに命名し、 同一クラス内のメソッド呼び出しや
オブジェクトからクラスの外の関数への受渡しを許可する。 言い替えれば
C++/Java の this
や Perl の $self
と全く同じである。
もし自分自身を参照する必要がいないのであれば、 (self)
部分を完全に省略できる -- 実際上の例は全て省略可能であろう。
だが私はこれを書いておくようアドバイスする。 いつクラスを変更して self
を参照する必要が出るかは分からないのだから。
書いておくペナルティはまったくない。
継承と型変換
# let b = new button "button";;
val b : button = <obj>
# let l = new label "label" "Press me!";;
val l : label = <obj>
# [b; l];;
Error: This expression has type label but an expression was expected of type
button
The first object type has no method add
ボタン b
とラベル l
を作り、
両方を含むリストを作ろうとした、がエラーになった。 b
も l
も
widget
なのだが、 何故同じリストに入れられなかったのだろう?
たぶん、OCaml は widget list
が欲しいことを推論してくれないのだろう。
では教えてみよう:
# let wl = ([] : widget list);;
val wl : widget list = []
# let wl = b :: wl;;
Error: This expression has type widget list
but an expression was expected of type button list
Type widget = < get_name : string; repaint : unit >
is not compatible with type
button =
< add : widget -> unit; get_name : string;
get_widgets : widget list; press : unit; release : unit;
repaint : unit >
The first object type has no method add
OCaml
はデフォルトではサブクラスをスーパークラスの型に変換しないことがわかる。
かわりに :>
(型変換) 演算子を使うと教えてやることができる。
# let wl = (b :> widget) :: wl;;
val wl : widget list = [<obj>]
# let wl = (l :> widget) :: wl;;
val wl : widget list = [<obj>; <obj>]
式 (b :> widget)
は 「ボタン b
は widget
型を持つように変換せよ」という意味だ。
コンパイル時にこの型変換が成功するかが完全に判断可能なので、
型安全は保存される。
実際、型変換は上で記述したよりは幾分微妙だから、 詳細を全部を探すにはマニュアルを読むように。
上で定義される container#add
メソッドは実際誤っており、 container
に別の型のウィジェットを加えようとしても失敗する。
型変換によって修正される。
スーパークラス(例えばwidget
) からサブクラス(例えばbutton
)への
型変換は可能だろうか? 答えは、たぶん驚くことに NO だ!
この向きの型変換は安全ではない。 widget
を button
じゃなくて実際には label
に型変換しようとしてしまうかもしれない。
スーパークラスからサブクラスへの型変換の問題は、Javaプログラマには馴染だ。
Java のコンテナ型は Object
を含んでおり、
オブジェクトをコンテナから抽出するときには元の型でキャストしないといけない。
これは ClassCastException
を実行時に引き起こし得る。 OCaml
の強い型システムは実行時の型エラーを除去する明確なゴールがあるので、
この種の型変換は許容されない。
多相と関数プログラミングによってスーパークラスから
サブクラスへの型変換の必要性をほとんどなくしている。 Java
のコンテナクラスで Object
を格納するのは、 Java が (コーディング時に)
ジェネリック (C++ の「テンプレート」や OCaml の多相) がないからである。
これは Java 言語の欠点 -- 実際とても基本的な欠点 --- であり、
うまくいけば Java 1.5 で修正されるだろう。 OCaml では 'a list
や
'a stack
のような多相型の定義がとても簡単なので、 Java
のようなプログラミングは全然要求されない。
こう言ったように、もしあなたがOCaml で大規模な OOP をするなら、
この変換型が実際にとても役立つケースを思いつくことを確信している。
たぶんこの理由から、本当にこれを理解したらまずはじめに関数型で
実装解決しようとして、また OO スタイルは限られた問題領域だけにする
といっていいだろう。
[山形頼之氏が型安全なダウンキャストが可能であると書いている。 上級ユーザは見よ: http://caml.inria.fr/pub/ml-archives/caml-list/2002/05/a6520926c4eac029206a31d6aa22f967.fr.html 。ここから hweak ライブラリが出来た]
Oo
モジュールとオブジェクトの比較
Oo
モジュールには OOP のための役に立つ関数がある。 Oo.copy
でオブジェクトの浅いコピーを作る。 Oo.id object
はオブジェクトそれぞれに固有のID値を返す (固有値はクラス全体で)。
=
と <>
はオブジェクトの物理的な等価比較に使う
(オブジェクトとそのコピーは物理的には同一ではない)。 <
などもオブジェクトの固有IDに基づいたオブジェクトの順列決定に使える。
クラスを使わないオブジェクト
ここではほとんどレコードのような、必ずしもクラスを使わない オブジェクトの使いかたを調べる。
直接オブジェクトとオブジェクトの型
オブジェクトはレコードのかわりに使え、 いくつかのケースでレコードよりも選択したくなるような良い特性がある。 オブジェクトを生成する正規の方法が、 まずクラスを定義してからそのクラスを用いて個別のオブジェクトを生成する ことであると見てきた。 これは場合によっては扱いづらことがある。 クラス定義は型定義よりも多く、また型による再帰定義が出来ないからだ。 だが、オブジェクトは型を持っており、 つまりレコード型にとても類似しており、型定義として使うことができる。 さらにオブジェクトはクラス無しで生成できる。 これは直接オブジェクトと呼ばれる。 次に直接オブジェクトの定義を示す:
# let o =
object
val mutable n = 0
method incr = n <- n + 1
method get = n
end;;
val o : < get : int; incr : unit > = <obj>
このオブジェクトには、パブリックメソッドだけが定義された型がある。 値およびプライベートメソッドは不可視である。 レコードとは異なり、このような型は事前に明示的に定義される必要はないが、 そうすることでより明確になる。 次のようにできる:
# type counter = < get : int; incr : unit >;;
type counter = < get : int; incr : unit >
等価なレコード型の定義と比較せよ:
# type counter_r = { get : unit -> int;
incr : unit -> unit };;
type counter_r = { get : unit -> int; incr : unit -> unit; }
このオブジェクトと同様に動作するレコードの実装は以下の通りだろう:
# let r =
let n = ref 0 in
{ get = (fun () -> !n);
incr = (fun () -> incr n) };;
val r : counter_r = {get = <fun>; incr = <fun>}
関数型の点においてはオブジェクトもレコードも似ているが、 これらの方法にはそれぞれ利点がある:
- スピード: レコードのフィールドアクセスが若干速い
- フィールド名: 異なる型のレコード操作はフィールドが同一の名前だと不便であるが、オブジェクトにはそのような問題はない。
- サブタイプ:フィールドがより少ない型へのレコードの型強制は不可能であるが、オブジェクトでは可能だ。なので、メソッドを幾つか共有する異なる種類のオブジェクトにおいて、共通のメソッドだけが可視であるようデータ構造を混在できる。(次の節を見よ)
- 型定義: あらかじめオブジェクト型を定義する必要はないので、モジュール間の依存性を軽くできる。
クラス型 vs. 単なる型
クラス型とオブジェクトの型の混同に注意せよ。 クラス型はデータ型ではなく、 型として通常参照される OCaml 用語である。 オブジェクトの型はデータ</em>型の一種であり、 レコード型やタプルと同じようなものである。
クラスが定義されると、クラス型とオブジェクトの型の両方が 同じ名前で定義される。
# class t =
object
val x = 0
method get = x
end;;
class t : object val x : int method get : int end
この例では、t
はこのクラスが生成するオブジェクトの型でもある。
異なるクラスから派生した、あるいはクラスでないオブジェクトでも
同じ型である限りいっしょに混ぜることができる。
# let x = object method get = 123 end;;
val x : < get : int > = <obj>
# let l = [ new t; x ];;
val l : t list = [<obj>; <obj>]
共通のサブタイプを共有する混在オブジェクトもできるが、 :>
演算子で型変換を明示する必要がある。
# let x = object method get = 123 end;;
val x : < get : int > = <obj>
# let y = object method get = 80 method special = "hello" end;;
val y : < get : int; special : string > = <obj>
# let l = [ x; y ];;
Error: This expression has type < get : int; special : string >
but an expression was expected of type < get : int >
The second object type has no method special
# let l = [ x; (y :> t) ];;
val l : t list = [<obj>; <obj>]
もっとオブジェクト
OCaml のマニュアルの3章には、 正規のオブジェクトやクラスの参照について書いてある。 ここでカバーしなかったがマニュアルでカバーされる内容として:
- プライベートメソッド
- 複雑なコンストラクタ
- インタフェース
- 多重継承
- 多相メソッド
- 型変換についての詳細
- 関数オブジェクト
- オブジェクトの複製についての詳細
- 相互再帰なクラス
- バイナリメソッド
- フレンド