(Redirected from ja/objects)
OCamlはオブジェクト指向、命令型、関数型言語だ :-)。 これらのパラダイムを混ぜて、 手元の仕事にもっともふさわしい(あるいは慣れた) プログラミングパラダイムが使える。 本章では OCaml でのオブジェクト指向プログラミングに注目するが、 何故オブジェクト指向のプログラムを書きたいか、 あるいは書きたくないかについても話題にする。
オブジェクト指向プログラミングを示すのに、 教科書で用いられる古典的でばかげた例として、 スタッククラスがある。 これはいろんな意味でとてもひどい例だが、 オブジェクト指向 OCaml を書く基本をこれで紹介する。
整数のスタックを提供する基本コードがこれだ。 このクラスは線形リストで実装してある。
open List
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 = hd the_list in
the_list <- tl the_list;
result
method peek = (* peek メソッド *)
hd the_list
method size = (* size メソッド *)
length the_list
end;;
class name = object (self) ... end が name という 名前のクラスを定義する基本パターンだ。
クラスには the_list という名前の変更可能なインスタンス変数(定数ではない) がひとつある。これは基礎的な線形リストだ。 ちょっと分かりにくいかもしれないがちょっとしたコードを使って、 この変数を初期化(stack_of_ints のオブジェクトが生成されるたびに)する。 この ( [] : int list ) 式は「int list 型の空リスト」という意味だ。 単純な空リスト [] は 'a list 型、 つまり任意型のリストであったことを思いだそう。 欲しいのは int のスタックであって他の何者でもないのだから、 ここでは、型推論エンジンに対して一般的な「任意のリスト」ではなく 実際にはもっと狭い「int のリスト」ということを伝えている。 ( 式 : 型 ) という文法は、 その式がその型であることを意味する。 これは一般的な型キャストではない。というのも、 型推論エンジンを無効にするのではなく、 汎用の型をもっと特定のものに狭めるだけだからだ。 だから例えば ( 1 : float ) とは書けない。
# (1 : float);; This expression has type int but is here used with type float
型安全は保存される。じゃあ例に戻ろうか...
このクラスには4つの単純なメソッドがある。 push は整数をスタックにプッシュする(積む)。 pop はスタックの先頭の整数をポップ(取り出し)してそれを返す。 <- (割り当て演算子)は変更可能なインスタンス変数の更新があるときに用いる。 同じく <- 割り当て演算子でレコード内の破壊可能フィールドの更新にも用いる。
peek はスタックに影響を及ぼさずに先頭(つまりリストの先頭)を返し、 size はスタックの要素数(つまりリストの長さ)を返す。
この整数スタックをテストするコードを書いてみよう。 まず新しいオブジェクトを生成しよう。 new 演算子というお馴染のものが使える。
let s = new stack_of_ints;;
ではいくつかの要素をスタックにプッシュしてポップしよう。
for i = 1 to 10 do s#push i done;; while s#size > 0 do Printf.printf "Popped %d off the stack.\n" s#pop done;;
文法に注意しよう。 object#method は object の method を呼び出すという意味だ。 これは他の命令型言語でいうところの object.method やら object->method と同じことだ。
このプログラムを実行するとこう表示する:
$ ./stack 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.
OCaml のトップレベルだとオブジェクトやメソッドの型が詳細に分かる。
# let s = new stack_of_ints;; val s : stack_of_ints = <obj> # s#push;; - : int -> unit = <fun>
s は不明確なオブジェクトだ。 実装(つまりリスト)は呼び出し側から隠されている。
整数のスタックはこれでいいが、どんな型でも格納できるスタックはどうだろう? (混合型を格納できる1つのスタックでなく、任意の一つの型のオブジェクトを 格納できる複数のスタックだ)。 'a list という感じで 'a stack を定義しよう。
open List
class ['a] stack =
object (self)
val mutable the_list = ( [] : 'a list ) (* instance variable *)
method push x = (* push method *)
the_list <- x :: the_list
method pop = (* pop method *)
let result = hd the_list in
the_list <- tl the_list;
result
method peek = (* peek method *)
hd the_list
method size = (* size method *)
length the_list
end;;
class ['a] stack は実際には一つのクラスを定義しているのではなく、 可能な型について全部の「クラスのクラス」を定義している (つまり無限に多くのクラスだ!)。 じゃぁ 'a stack クラスを使ってみよう。 この例ではスタックを生成して浮動小数点数をこのスタックにプッシュする。 スタックの型に注意しよう。
# let s = new stack;; val s : '_a 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";; This expression has type string but is here used with 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;;
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;;
このコードには幾つかあたらしいことがある。 まず、このクラスには初期化子が含まれている。 ちょうど例えば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";; One cannot create instances of the virtual class widget
この container クラスはもっと面白くなる。 widget から継承されなければならず、 ウィジェットを含むリストを格納する仕掛けを持たなければならない。 次に単純な container の実装を示す;
open List
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 =
iter (fun w -> w#repaint) widgets
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)
一つの引数 w を伴う匿名関数を定義して w#repaint (ウィジェット w の repaint メソッド) を呼び出している。
この例では button クラスは単純である (むしろ実際には非現実的な程単純だが気にしない):
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;;
注意:
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 # b#press;; Ouch! # b#repaint;; Button being repainted, state is Pressed # b#release;;
ここに比較的平凡な label のクラスがある:
class label name text =
object (self)
inherit widget name
method repaint =
print_endline ("Label: " ^ text)
end;;
「Press me!」というラベルを作ってボタンに加えよう。
# let l = new label "label" "Press me!";; val l : label = <obj> # b#add l;; # b#repaint;; Label: Press me! Button being repainted, state is Released
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];;
This expression has type label = < get_name : string; repaint : unit >
but is here used with type
button =
< add : widget -> unit; get_name : string; get_widgets : widget list;
press : unit; release : unit; repaint : unit >
Only the second object type has a method add
ボタン b とラベル l を作り、 両方を含むリストを作ろうとした、がエラーになった。 b も l も widget なのだが、 何故同じリストに入れられなかったのだろう? たぶん、OCaml は widget list が欲しいことを推論してくれないのだろう。 では教えてみよう:
# let wl = ([] : widget list);; val wl : widget list = [] # let wl = b :: wl;; This expression has type widget list but is here used with type button list
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_r = { get : unit -> int;
incr : unit -> unit }
このオブジェクトと同様に動作するレコードの実装は以下の通りだろう:
let r =
let n = ref 0 in
{ get = (fun () -> !n);
incr = (fun () -> incr n) }
関数型の点においてはオブジェクトもレコードも似ているが、 これらの方法にはそれぞれ利点がある:
クラス型とオブジェクトの型の混同に注意せよ。 クラス型はデータ型ではなく、 型として通常参照される 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 ];;
^
This expression has type < get : int; special : string >
but is here used with type < get : int >
Only the first object type has a method special
# let l = [ x; (y :> t) ];;
val l : t list = [<obj>; <obj>]
OCaml のマニュアルの3章には、 正規のオブジェクトやクラスの参照について書いてある。 ここでカバーしなかったがマニュアルでカバーされる内容として: