関数型言語とオブジェクト指向は相容れない,という説をよく聞く。たしかに「オブジェクトは状態を持つ」「関数型プログラミングでは,できるだけ破壊的代入を行わない」とすれば,二つの概念は矛盾しているようにも思われる。また,技術的観点以外にも,「とかくシンプルさを好む多くの関数型言語プログラマが,何かと物事を複雑にする(と思われている)オブジェクト指向を嫌っている」という面があるかもしれない。
しかし,個人の好き嫌いはさておき,実際問題として,関数型言語とオブジェクト指向は大いに関係がある。むしろ,基礎理論については,ほとんど同じコミュニティの人たちが取り組んでいる,と言ってもいい。例えば,以下のような研究が,1980年代から現在に至るまで行われている。
- 関数型言語のモデルであるλ計算という体系において,オブジェクトを表現する研究(参考リンクなど)
- λ計算にならい,(プロトタイプベースの)オブジェクト指向言語のモデルとしてオブジェクト計算を定義した研究(参考リンクなど)
- λ計算やオブジェクト計算にならい,Javaのようなクラスベースのオブジェクト指向言語のモデルを定義した研究(参考リンクなど)
これらの研究では,「状態」を関数の引数や返値として受け渡しすることにより,「破壊的代入」を用いずにオブジェクトを表している(破壊的代入を用いるモデルもあるが,それでも関数型言語において発達した「帰納的定義」「操作的意味」といった手法が用いられている)。
ちなみに,上述の3番目の研究は,JavaにおけるGenerics(関数型言語でいうところの多相型)の導入・拡張にも関連しており,この研究により京都大学の五十嵐淳助教授は本年の日本IBM科学賞を受賞した(参考リンク)。ただ,JavaのGenericsそのものは後方互換性のために仕様が複雑となっており,コミュニティでの「受け」は必ずしも良くないようだ。
OCamlのオブジェクト
さて,関数型言語「ML」の一種であるOCamlにも,オブジェクト指向の機能がある(そもそも"OCaml"は"Objective Caml"の略なのだから当たり前だ)。例えば,文字列"Hello\n"を返すメソッドgetと,その文字列を表示するメソッドprintを持つオブジェクトhelloは,下のように定義できる。
# let hello = object (self) method get = "Hello\n" method print = print_string self#get end ;; val hello : < get : string; print : unit > = <obj> #
ただし,print_stringは,OCamlであらかじめ定義されている,文字列を表示する関数である。一般にメソッドm1, m2, ...を持つオブジェクトは,以下のような形の式で表現される。
object (self) method m1 = 式1 method m2 = 式2 ... end
また,オブジェクトxのメソッドmを呼び出すには,x#mと書けばよい。例えば,helloオブジェクトのprintメソッドは以下のようにして呼び出せる。
# hello#print ;; Hello - : unit = () #
なお,上のhelloの定義で,object (self)のselfは「自分自身」を表す変数である。したがって,メソッドprintの定義の中のself#getは,自分自身のgetメソッドの呼び出し(つまり"Hello\n")になる。
同様に,例えば整数123と,それを表示するメソッドを持つオブジェクトは,下のように定義できる。
# let int123 = object (self) method get = 123 method print = print_int self#get end ;; val int123 : < get : int; print : unit > = <obj> # int123#print ;; 123- : unit = () #
型推論とオブジェクトの多態性
さて,前回までの変数や関数の定義で気づかれた方も多いかと思うが,OCamlでは,プログラマが宣言しなくても変数や関数の型は自動で推論される。これを型推論という。
オブジェクトも例外ではない。上述のhelloオブジェクトは,「文字列を返すメソッドgetと,unit(C言語のvoidのようなもの)を返すメソッドprintを持つ」と推論されている。int123オブジェクトも同様である。
では,メソッド呼び出しの型は,どのように推論されるのだろうか? 例えば,「オブジェクトxを受け取って,xのprintメソッドを呼び出す」関数を定義してみよう。
# let invoke_print x = x#print ;; val invoke_print : < print : 'a; .. > -> 'a = <fun> #
< print : 'a; .. >という型は,「何らかの型'aの値を返すメソッドprintを持ち,それ以外にもメソッドがあるかもしれない」というオブジェクトを表す。関数invoke_printは,そのような型のオブジェクトを受け取ったら,printメソッドを呼び出すので,'a型の値を返すわけだ。
この関数は,helloオブジェクトにもint123オブジェクトにも適用できる。invoke_printの型で,「何らかの型'a」にunitが,「それ以外のメソッド」にgetがあてはまり,型検査が成功するわけだ。
# invoke_print hello ;; Hello - : unit = () # invoke_print int123 ;; 123- : unit = () #
ここで,(ひょっとしたらJavaやC++に慣れている人は驚くかもしれないが)型宣言だけでなく,インタフェースや継承などの宣言も,全く必要なかったことに注意してほしい。このように,OCamlのオブジェクトおよびメソッド呼び出しは,型推論により静的型を与えられているが,動的型のオブジェクト指向言語にも似た多態性を持っている。
ちなみに,このようなメソッド呼び出しは,OCamlでは一種のハッシュ・テーブルを用いて実装されている。メソッド名のハッシュ値はコンパイル時に計算されるので,実行時には,メソッド名の長さやメソッド数にかかわらず,ほぼ定数時間でメソッド呼び出しが行われる。そのコストは数十CPUサイクル程度で,現代のCPUでは(C言語でいうところの)関数ポインタを介した関数呼び出しより少し遅い程度とされている。
構造的部分型
上述のように,OCamlは,クラスや継承・インタフェースなどを宣言しなくても,オブジェクトやメソッドの型を自動で推論する。部分型関係(いわゆるis-a関係の一種)も,実際のオブジェクトの型にしたがい,あらかじめ宣言をしなくても成立する。これを構造的部分型(structural subtyping)という。逆に,JavaやC++のように,あらかじめ宣言しなければ成立しない部分型関係を名前的部分型(nominal subtyping)と呼ぶ。
例えば,printメソッドを持つオブジェクトの型をprintableと呼ぶことにしよう。
# type printable = < print : unit > ;; type printable = < print : unit > #
type printable = ...という構文は,右辺の型を「printable」と名付ける,という宣言である。また,< print : unit >という型は,unit型のメソッドprintを持つオブジェクトの型である。
すると,前出のhelloオブジェクトもint123オブジェクトも,printable型を持つとみなすことができる。
# (hello :> printable) ;; - : printable = <obj> # (int123 :> printable) ;; - : printable = <obj> #
(x :> t)は,式xの型tへのアップキャストを表す。前後の括弧は省略できないので注意してほしい。アップキャストが型エラーを起こさないことから,helloやint123の型と,printableとの間に,部分型関係が成立していることがわかる。
ちなみに,鋭い方は「先のinvoke_printのように,アップキャストをしなくてもprintメソッドを呼び出せるなら,何のためにアップキャストが要るのか」と思われるかもしれない。OCamlでのアップキャストは,例えば同じ配列やリストの中に,helloとint123の両方のオブジェクトを格納する場合などで必要になる。これは,そのような場合の型推論が理論的に困難なためである。
クラスの定義と実装の継承
ここまでは,クラスを宣言せず,object ... endという構文ですべてのオブジェクトを直接定義してきた。が,OCamlでも,JavaやC++のように,クラスを宣言してオブジェクトを生成することもできる。
# class strobj x = object (self) method get = x method print = print_string self#get end ;; class strobj : string -> object method get : string method print : unit end # let hello = new strobj "Hello\n" ;; val hello : strobj = <obj> # hello#print ;; Hello - : unit = () #
上の例では,「class strobj x = ...」という構文でstrobjクラスを定義し,「new strobj "Hello\n"」により,xとして"Hello\n"を与え,strobjクラスのオブジェクトのインスタンスを作っている。
もちろん,オブジェクトの生成だけが目的ならば,クラスを定義してnewするかわりに,以下のようにxを引数として受け取り,オブジェクトを返す関数を書いても(型の表示のされ方など以外は)ほぼ同じことができる。
# let new_strobj x = object (self) method get = x method print = print_string self#get end ;; val new_strobj : string -> < get : string; print : unit > = <fun> # let hello = new_strobj "Hello\n" ;; val hello : < get : string; print : unit > = <obj> # hello#print ;; Hello - : unit = () #
しかし,クラスを定義すれば,オブジェクトの生成(new)だけでなく,実装の継承が可能となる。例えば,strobjクラスに文字列の長さを返すメソッドlengthを追加して拡張したければ,次のように書くことができる。
# class strobj2 x = object (self) inherit strobj x as super method length = String.length self#get end ;; class strobj2 : string -> object method get : string method length : int method print : unit end # let hello2 = new strobj2 "Hello\n" ;; val hello2 : strobj2 = <obj> # hello2#length ;; - : int = 6 #
上の例では,「inherit strobj x as super」が「strobjクラスを継承し」かつ「継承する親クラスのインスタンスをsuperと呼ぶ」という宣言である(この例に限れば「as super」の部分は不要だが,多重継承やメソッドのオーバーライディングなどによって,親クラスのメソッドが隠れてしまう場合には必要になる)。
継承≠部分型関係
さて,ここからはやや高度な話題になる。一般に,実装の継承と(is-a関係が成立するという意味での)インタフェースの継承は異なることに気を付けたい。例えば,古典的な例として,以下のような場合がある。
# class point (pos : int) = object (self : 'self_type) method get_pos = pos method is_equal_to (p : 'self_type) = (p#get_pos = self#get_pos) end ;; class point : int -> object ('a) method get_pos : int method is_equal_to : 'a -> bool end # class colored_point (pos : int) (color : int) = object (self : 'self_type) inherit point pos as super method get_color = color method is_equal_to (p : 'self_type) = (p#get_pos = self#get_pos) && (p#get_color = self#get_color) end ;; class colored_point : int -> int -> object ('a) method get_color : int method get_pos : int method is_equal_to : 'a -> bool end # let cp = new colored_point 123 255 ;; val cp : colored_point = <obj> # (cp :> point) ;; Characters 1-3: (cp :> point) ;; ^^ This expression cannot be coerced to type point = < get_pos : int; is_equal_to : point -> bool >; it has type colored_point = < get_color : int; get_pos : int; is_equal_to : colored_point -> bool > but is here used with type < get_pos : int; is_equal_to : point -> bool; .. > Type colored_point = < get_color : int; get_pos : int; is_equal_to : colored_point -> bool > is not compatible with type point = < get_pos : int; is_equal_to : point -> bool > Only the first object type has a method get_color #
一見すると複雑だが,要約すると上のコードは以下のような意味になっている。
- pointクラスは,メソッドget_posとis_equal_toを持つ
- colored_pointクラスは,pointクラスを継承して,メソッドget_colorを追加し,is_equal_toをオーバーライドしている
このようにcolored_pointはクラスを継承しているが,「# (cp :> point) ;;」の実行結果が示すように,pointのサブクラス(部分型)にはならない。もし仮にcolored_pointがpointのサブクラスだったら,下のようなプログラムが通ってしまい,p#get_colorのようなありえないメソッド呼び出しが起こってしまうためだ。
# let p = new point 456 ;; val p : point = <obj> # let cp = new colored_point 789 255 ;; val cp : colored_point = <obj> # (cp :> point)#is_equal_to p ;; (* 実際には型エラーになり実行できない *)
この現象は,is_equal_toのようなバイナリメソッド(自分と同じ型のオブジェクトを引数として受け取るメソッド)が原因で,いわゆる「inheritance is not subtyping」問題として広く知られている(http://portal.acm.org/citation.cfm?id=96721&dl=ACM&coll=portal)。このような問題があるので,「インタフェースの継承は必要だが,実装の継承は有害である」という議論すらあるぐらいだ。
より正確には,「(実装の)継承とis-a関係を混同してはいけない」というべきだろう。「inheritance is not subtyping」問題からもわかるように,継承したからといって必ずしもis-a関係が成り立つとは限らないからだ。逆に,先のprintableの例からもわかるように,継承しなければis-a関係が成立しない,というわけでもない。だとすれば,C++やJavaといったメインストリームのオブジェクト指向言語や,UMLなどのソフトウエア工学手法が,継承を中心に設計されていることにも疑問がわく。現に「継承は使うな」というコーディング規約を掲げている有名企業もあると聞く。禁止までいかなくても,あまり継承を使わない,というプログラマは実際に少なくないだろう。継承を使わないとしたら,(より単純な関数型言語や命令型言語ではなく)オブジェクト指向言語を用いる意義は何なのか。社会的慣性の問題なのか,あるいはそれ以外の本質的理由があるのか。すでに世の中に広まったかのように見えるオブジェクト指向だが,まだまだ未知の側面も大きいと思う。
著者紹介 住井 英二郎 東北大学 大学院 情報科学研究科 助教授。「オブジェクト指向の何が良いのか」という疑問は,1960年代のSimula言語の時代からあるようですが,いまだにはっきりとした答えは出ていないように思われます(特に通常の抽象データ型ないしモジュール・システムや,関数型言語のクロージャなどと比較した場合)。もしあいまいな理解により,流行だけで「オブジェクト指向っぽいもの」が強制され,現場のエンジニアやプログラマが苦労しているとしたら,それを解消することは「研究者」や「専門家」の責任かもしれません(率直にいうと自分はオブジェクト指向のエキスパートではないので,無知による誤解ないし偏見かもしれませんが…)。 |