このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

牛が牧草を食うのが共変継承なのか?

Eiffelというプログラミング言語での話ですが、「反変(contravariant)の継承(反変規則による継承)」と「共変(covariant)の継承(共変規則による継承)」という奇妙な概念とそれに関する議論があります。

「牛は草食動物のサブクラス」なんて、僕が嫌がる例が出てきて(「僕は、オブジェクトも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のサブタイプであるとき:

  1. Derivedの値やオブジェクトxがあるとき、自然な型変換により、型がBaseである値/オブジェクトx'が存在する。
  2. 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) {
  // ...
 }
 // ...
}

classextends 草食動物 {
 public void eat(牧草 food) {
  // ...
 }
 // ...
}

出たよ、草食動物と牛だよ。現実の問題が牛か、君は牧場経営者か? まー、それはともかく、「草食動物→牛」とサブクラスを作っていて、eatの引数型も同じ方向に「植物→牧草」とサブクラスに変更しているので、これは共変の継承の例になっていますね。

以上で、牛と継承の問題(じゃねーよ、反変の継承と共変の継承の問題ね)の説明は済みました。牛が出てきた段階で、僕は思考停止(つうより思考放棄)しちゃうんだけど、そもそも、継承の枠内で議論するのが変なんじゃないの。問題設定がズレてるから、考えるだけ不毛だとも言えるが、僕なりに牛を使わない解決と説明をしましょう -- 今日は余裕がないから次回に。