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

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

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

参照用 記事

牛が牧草を食うことを、総称の文脈で語ってみよう

「牛が牧草を食うのが共変継承なのか?」の続きです。共変性(covariance)の問題を考えてみます。

まず、補足説明から

誤解を避けるため、いくつかの補足をしておきます。

  1. キッカケはEiffel FAQだが、Eiffel(あるいは他の何か特定言語)の話題ではなくて、一般的な話。
  2. Eiffel FAQのなかに、「共変規則」、「反変規則」という言葉は出てくるが、「共変(反変)の継承」という言い方は僕が採用したもの;“共変(反変)規則に従った継承”の意味です。
  3. 牛の例は、Eiffel FAQにほんとに書いてあります!

この話題の焦点は、サブクラスにおけるメソッドのオーバライド(引数型を変更しての再定義)に関する事柄だから、「共変(反変)オーバライド」、「共変(反変)再定義」とかの言葉が適切かもしれないですね。でも、「共変(反変)の継承」は、このエントリーでも使い続けます。

牛の継承は変な感じがする

継承における「反変 vs 共変」のハナシを、「混乱した奇妙な議論」と僕は言ったけど、問題設定/定式化がなんかオカシイと思うのですよ。なんでもかんでも“継承”という枠内で考えよう、解決しようって感じがするわけ。プログラミング言語が提供する機能が継承しかないなら、それも致し方ないけど、今やインターフェースや総称(generics)が(一部の言語では)利用可能だし、概念的には形式仕様だって使えますよ。継承に固執しなくてもいいでしょ。

それで、共変の継承だとされている事例の一部は(全部とは言わない)、継承でも何でもなくて、型パラメータの具体化に過ぎないのじゃないかと思うのですよ。あの牛の例も、型パラメータの具体化だろうと感じるのだけど:

class 草食動物 {
 public void eat(植物 food) {
  // ...
 }
 // ...
}

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

牛に詳しくない僕には、この例を扱う能力はありません。別な例にします。

class List {
 public void add(Object item) {
  // ...
 }
 // ...
}

class IntegerList extends List {
 public void add(Integer item) {
  // ...
 }
 // ...
}

共変の継承の問題点:再確認

上のList(常識的なリストだと思ってください)の例で次のコードは何の問題もありません。

 List list = new List();
 list.add("Hello");
 list.add(new Date());

ここで、List型インスタンスの代わりにIntegerList型インスタンスを使用してみます。

 List list = new IntegerList();
 list.add("Hello");
 list.add(new Date());

IntegerList型では、追加できる項目を型により制約しているので、list.add("Hello");list.add(new Date());も許されません。とは言っても、コンパイラがこのテの問題を完全に検出するのは不可能なので、実行時の型エラーとなるでしょう。

リスコフの置換原則(の教訓としての解釈)は、「スーパータイプのインスタンスを、サブタイプの対応するインスタンスに置き換えても、何の問題も起きないようにせよ」と主張しているので、IntegerList extends List という共変の継承はリスコフの置換原則を破っています。

「リスコフの置換原則が、そんなにエライのかよ、えっ、おい?」とか、「継承とサブタイプ/スーパータイプは別物と考えよう」とかの意見はあるでしょうが、ここでは次のことを原則にしておきます。

  1. サブタイプ/スーパータイプに関して、リスコフの置換原則は守る。
  2. 継承があれば、サブタイプ/スーパータイプの関係もあるとみなす。

これが僕の信条ってわけではないけど、何かの基準がないと話を進められないですからね。

総称リスト型とその具体化

ListとIntegerListの関係は、継承とみるよりは、次のような総称(型パラメータ付き)リスト型の2つの具体化とみなすほうが自然な気がします。

interface List<E> {
 public void add(E item);
 // ...
}

List<E>はクラスではなくてインターフェースであり、ArrayList<Integer>などが実際の具体的リスト実装となります(Javaのコレクション・ライブラリがこうなってます)。

さて、問題は、List<Object>とList<Integer>の関係です。IntegerがObjectのサブタイプになっているので、List<Integer>もList<Object>のサブタイプになっているような気がします。が、既にみたように、List<Integer>とList<Object>はリスコフの置換原則を破るので、サブタイプ/スーパータイプだとは認められません。これは、継承として定式化しようが、総称の具体化として定式化しようが、それに関係なく、事実として原則違反があるからダメってことです。

それでは、List<Object>がList<Integer>のサブタイプになっている(型の上下関係が逆転の)可能性はどうでしょうか? これもダメです。次のコードを見てください。

 // オートボクシング/アンボクシングが働くとする
 List<Integer> list = new ArrayList<Integer>();
 // listへの操作
 // あれこれ
 int x = list.get(3);

コンパイラは、int x = list.get(3);の行で(IndexOutOfBoundsExceptionの発生はあり得るが)型エラーは起きないことを確信できます。しかし、ArrayList<Integer>をArrayList<Object>に置き換えると(それはできるとしても)、キャストがないint x = list.get(3);を許すことはできません。このように取り扱いを変えなくてはいけないのでは、とてもサブタイプなんて呼べません。

法則性を期待しても、そうじゃないのだから仕方ない

以上のことから、FがEのサブタイプであっても:

  • List<F>がList<E>のサブタイプとはならない。
  • List<E>がList<F>のサブタイプともならない。

ことがわかりました。これは何か問題でしょうか?

型パラメータ付きのリスト型を、型を受け取って新しい型を返す一種の関数のように考えましょう。Listは、E → List<E> という対応を与えるわけです。いま臨時に集合の記号を拝借して F⊆E で「FがEのサブタイプ」を表すとすると、上に述べたことは次のように表現できます。

  • F⊆Eであっても、List<F>⊆List<E> ではない。
  • F⊆Eであっても、List<E>⊆List<F> ではない。

一般に、型を受け取って新しい型を返す関数Tが「E⊆F ならば T⊆T」のとき共変に単調、「E⊆F ならば T⊆T」のとき反変に単調と呼ぶことにすると。

  • Listは共変に単調ではない。
  • Listは反変に単調ではない。

となります。これは事実なので、文句を言っても仕方ありません。すべての型構成子(型を受け取って新しい型を返す関数)が単調なんてことはないのです。普通の関数に関しても、「すべての関数が単調(増加関数または減少関数)」なんて法則は聞いたことないでしょう。

これで解決したわけではない

さて、共変の継承が必要そうな事態が起きたとき、いつでもそれを総称と具体化で定式化し直せるか、というと、それは保証できません。そんなにうまくいくとは思えません。

例えばEiffelの設計者であるメイヤー(Bertrand Meyer)は、ヨットとカタマラン(双胴艇)という例を出してます。カタマランはヨットのサブクラスであり、カタマランの乗員(スキッパーやクルー)は、たんなるヨット乗りではなくて、カタマラン乗り(ヨット乗りのサブクラス)だそうです。ヨットを総称化(パラメータ化)して、その具体化としてカタマランを定義するのは、確かに難しそうな感じです。感じがするだけでよくわかりません。僕、カタマラン、あまり詳しくないし。

メイヤー先生の著書にはお世話になりました。随分と影響も受けました。でもでも、だけどね、牛*1だのカタマランだの、もう、そういう例出すのやめてくださいよ! ジェンジェン分からんもん。

カタマランはともかくとして、クラス/型のあいだの関係が、継承関係なのか、仕様と実装の関係か、それとも総称型とその具体化なのか、判断に苦しむことがあります。考えるべきことはまだあるってことですが、とりあえず僕は言っておきたい -- 牛で考えるのはやめよう。

*1:牛はメイヤー先生ご本人の例ではないようだけど。