Eiffelというプログラミング言語での話ですが、「反変(contravariant)の継承(反変規則による継承)」と「共変(covariant)の継承(共変規則による継承)」という奇妙な概念とそれに関する議論があります。
- http://www.cs.purdue.edu/homes/bahmutov/cs510/eifell.html -- 反変/共変の議論
- http://www.faqs.org/faqs/eiffel-faq/ -- Eiffel FAQの全体
- http://www.geocities.co.jp/SiliconValley/8632/EiffelFAQ.html#LCON -- 日本語訳だが読みやすくない(機械翻訳?)
「牛は草食動物のサブクラス」なんて、僕が嫌がる例が出てきて(「僕は、オブジェクトもthisもサッパリ理解できなかった」を参照)、それでウンザリなんだけど、クラスと継承の混乱した(と僕には思える)議論の事例として紹介しましょう。
まず、以下の話は別にEiffel特有なものではありません。クラスと型(タイプ)に関する一般論です。とりあえず、反変の継承と共変の継承から説明しましょう。なお、説明用の構文(擬似コード)はJava風にします。
反変の継承と共変の継承
次の状況を考えます。
class Base { public void doSomething(Foo arg) { // ... } // ... } class Derived extends Base { public void doSomething(Bar arg) { // ... } // ... }
このとき、メソッドdoSomethingの引数の型FooとBarにどんな関係があるべきか、というのが今回のトピックです。「特定の言語でどうなっているか」が問題ではなくて、一般的ポリシーの議論ですね。
典型的な2つの規則として:
- BarはFooのスーパータイプであるべき。クラスの型と引数の型において、サブ/スーパ関係が反対方向になるので、これを反変規則と呼びます。
- BarはFooのサブタイプであるべき。クラスの型と引数の型において、サブ/スーパ関係が同じ方向になるので、これを共変規則と呼びます。
もちろん、「BarとFooは同じであるべき」とか「BarとFooは何でもよい」などの規則もありえますが、今回考えるのは反変、共変の2つの規則です。
リスコフの置換原則
リスコフ(Liskov)の置換原則は、サブタイプ/スーパータイプに関する基本的規則です。DerivedがBaseのサブタイプであるとき:
- Derivedの値やオブジェクトxがあるとき、自然な型変換により、型がBaseである値/オブジェクトx'が存在する。
- Base型のx'を使っているプログラムにおいて、x'をxに置き換えてもそのプログラムの動作に一切の影響を与えない。
この置換原則は、サブタイプを定義するとき、こうなるようにしなさいという教訓だとみなしてもいいでしょう。
具体的な例を出せば、次の(1)のコードが問題なく動いているとき、(2)のコードもまったく同じように動くべき、ということです。
// (1) 変数objにはBaseのインスタンス Base obj = new Base(); // なにやら、かにやら // (2) 変数objにはDerivedのインスタンス Base obj = new Derived(); // なにやら、かにやら
リスコフの置換原則からみた反変と共変
反変の継承の例を出しましょう(つまらん例だけど)。
class Printer { public void print(String arg) { // ... } } class PowerfulPrinter extends Printer { public void print(Object arg) { // ... } }
Printer#printは文字列しかプリント(表示か印刷か)できませんが、PowerfulPrinter#printは何でもプリントできます。Printer pr = new Printer();
の代わりにPrinter pr = new PowerfulPrinter();
としても、特に問題ありません(文字列のプリントの動作に変化がないとして)。
反変の継承では、派生クラス側で基底クラスの動作をキチンとエミュレートするなら、リスコフの置換原則を破ることはありません。
次の例はどうでしょう。
class Printer { public void print(Object arg) { // ... } } class TextPrinter extends Printer { public void print(String arg) { // ... } }
この場合Printerの利用者は、Printer#printに任意のオブジェクトを渡していいと思っているので、pr.print(new Date());
のように使うかもしれません。変数prにTextPrinterのインスタンスが入っていると、(言語により、コンパイル時か実行時のいずれかで)困ったことになります。
という次第で、リスコフの置換原則を基準に考えると、反変の継承はよいが、共変の継承はダメだ、ってことになります。
ここで、牛が登場
ところがしかし、現実の問題を考えるときは、共変の継承がしばしば登場するから共変の継承も許すべきだ、という議論があります。Eiffel FAQから事例を引用します。
class 草食動物 { public void eat(植物 food) { // ... } // ... } class 牛 extends 草食動物 { public void eat(牧草 food) { // ... } // ... }
出たよ、草食動物と牛だよ。現実の問題が牛か、君は牧場経営者か? まー、それはともかく、「草食動物→牛」とサブクラスを作っていて、eatの引数型も同じ方向に「植物→牧草」とサブクラスに変更しているので、これは共変の継承の例になっていますね。
以上で、牛と継承の問題(じゃねーよ、反変の継承と共変の継承の問題ね)の説明は済みました。牛が出てきた段階で、僕は思考停止(つうより思考放棄)しちゃうんだけど、そもそも、継承の枠内で議論するのが変なんじゃないの。問題設定がズレてるから、考えるだけ不毛だとも言えるが、僕なりに牛を使わない解決と説明をしましょう -- 今日は余裕がないから次回に。