Skip to content.

Sections
Personal tools
You are here: Home » コミュニティ » masarl memorial » homepage3.nifty.com » masarl » article » nifty-logs » Niftyの過去ログ集 - 共変性について

Niftyの過去ログ集 - 共変性について

Document Actions

共変性について

相対性理論は,テンソル解析という数学を使うのですが,これには共変ベクトル (covariant vector) や反変ベクトル (contra-variant vector) という言葉が出てきます.ものすごく直感的な説明をすると,共変ベクトルは座標変換で同じ向きに変換されるベクトル,反変ベクトルは反対向きに変換されるベクトル,ということです.

オブジェクト指向でも,共変 (covariant) と 反変 (contra-variant) という言葉が用いられていて,こちらの場合は,

  • ベクトルではなくオブジェクトの型
  • 座標変換ではなく継承

に対して共変,反変という言葉が用いられるようです.例えば,クラス Base はメソッド foo を持ち,Base の子クラス Derived も メソッド foo を持っているとします(って子クラスだから当たり前か).静的型付けのオブジェクト指向言語では,大抵の場合 Base::foo と Derived::foo のシグニチャは同じです.

さて,ここで foo メソッドの戻り値の型を,継承したときに変更してみるということを考えてみましょう.

class Base                 { public X foo(); }
class Derived extends Base { public Y foo(); }

このとき, クラス X と Y について

  • X が Y の親クラスなら, foo メソッドの戻り値は 共変
  • X が Y の子クラスなら, foo メソッドの戻り値は 反変

ということができます.foo メソッドを所有するクラスの継承の方向と,foo メソッドの戻り値の クラスの継承の方向を考えれば,共変か反変かわかっていただけるでしょうか.以上のことは foo メソッドの引数の型についても言えます.つまり,foo メソッドの引数の型を継承したときに変更してみるのです.

class Base                 { public void foo(X x); }
class Derived extends Base { public void foo(Y y); }

このとき,

  • X が Y の親クラスなら, foo メソッドの引数は 共変
  • X が Y の子クラスなら, foo メソッドの引数は 反変

となります.以上のことを踏まえて,次の発言を読んでみて下さい.

02890/02890 BYI20012  まさーる         RE^2:戻り値の型付きメソッドの継承
( 6)   99/04/18 20:19  02885へのコメント

小林 浩一 さん,こんにちは.

>  Eiffelではこのように,型が適合する限り,基底クラスのメソッドの
>パラメータおよび戻り値の型を派生クラスで変更することができます.
>MeyerはOOSC2で,このことを「COVARIANCE」と呼んでいます.
>英和辞書では「(物理法則の)共変性」とのことです.??

?? 戻り値はともかく,引数の型まで変えてしまったら,それはオーバーライ
ドではなく普通オーバーロードになってしまいますよね?

>b1, b2, b3: BASE;   -- BASE型の変数を宣言
>!DERIVED!b1;        -- DERIVED型のインスタンスを生成
>!!b2;               -- BASE型のインスタンスを生成
>b3 := b1.foo(b2);   -- DERIVEDのfoo()を呼び出す(b2がBASE型なのでNG!!)

うーん...
これをランタイムエラーとして処理してしまうのは言語仕様として全然納得でき
ませんが....なんか僕勘違いしてます?

                                     99/04/18(日) まさーる(BYI20012)

ちょっとこの部分だけではわかりにくいと思いますが,要するに Eiffel では,引数の型を共変に変えることができる,ということです.これには驚きました.というのも,これを許してしまえば Design by Contract や Liskov Substitution Principle を破っているように思えるからです.

02902/02902 BYI20012  まさーる         RE^4:戻り値の型付きメソッドの継承
( 6)   99/04/19 22:42  02893へのコメント

小林 浩一 さん,こんにちは.

>  引数の型を変えられると言っても,無制限に変えられるわけではなく,
>「型が適合する」場合だけ,オーバーライドとして扱われます.
>「適合する」のは,大雑把に言えば,基底型に対する派生型です.
>それ以外はオーバーロードになります.

これについては,Meyer先生のDesign by Contractそのもので反論しましょう
(非常に恐れ多いことですが(^^;).

DbCでは,BaseクラスとDerivedクラスのオーバーライドされたメソッドに関して

・Derivedクラスのメソッドの事前条件は,Baseクラスのそれより等しいか弱く
なければならない.

・Derivedクラスのメソッドの事後条件は,Baseクラスのそれより等しいか強く
なければならない.

ということがいえますよね.

一方で

・引数の型は,事前条件である
・戻り値の型は,事後条件である

と考えるのが自然でしょう.以上の2つの事実と,「子クラスの型は,親クラス
の型より条件がきつい」ということから,次のことが導かれます:

・Derivedクラスのメソッドの引数の型は,Baseクラスの引数の型と同じか,
Baseクラスの引数の型の*親クラス*でなければならない.

・Derivedクラスのメソッドの戻り値の型は,Baseクラスの引数の型と同じか,
Baseクラスの引数の型の*子クラス*でなければならない.

だから,戻り値の型は,メソッドを所有するクラスと同じ方向に,つまり
covariantに型を変えるというのは自然です
(というか静的型付け言語でprototypeパターンなど生成に関するパターンを適
用するにはこの機能はないと困るでしょう).

一方で,引数の型をcovariantに型を変えるというのは,事前条件がおかしくな
るので方向が逆だと思います.contra-variantに型を変えるのはOKだと思います.
(contra-variantに型を変えたらどういう意味があるのか僕にはわかりません).

とはいえ,あえてcovariantにしたのは他に理由があるからなんでしょうね.ひ
ょっとしてmultiple-dispatch実現ためかなあ?(注:単なる思い付き) 
代入演算子は引数もcovariantでないと困るのではなかろうか,と思ったり...
うーん..でもオーバーライド対象なのかなあ.

やっぱOOSC2を読まないと(いつになることやら...^^;)

                                     99/04/19(月) まさーる(BYI20012)

その後,小林さんから,これが成り立つのは is-a である場合ですよね,と指摘されました.この発言に驚いたのは,is-a 関係が成り立たない場合でも継承を使ってよい,というふうに聞こえたからです.

02911/02911 BYI20012  まさーる         Covariance
( 6)   99/04/21 23:03  02907へのコメント

小林 浩一 さん,こんにちは.

>  これが成立すべきなのは,is-aである場合ですよね.

これを見て えっ? と思う人は結構いるのでは(^^;.

実は,小林さんの意見も納得できるのでずっと考えていたんですが,だんだんそ
ういう考え方もありかなあという気がしてきました.

それで気になって http://www.eiffel.com から

"Static typing and other mysteries of life", Bertrand Meyer

という記事を読んでみました(OOSC2が手元になかったので).

これによると,やはり僕と同じ考えのcontravariantを主張する人たちもいるみ
たいです.ですが,Meyerはcontravariantな人たちを結構批判してますね.現実
に合わない,ということでしょうか.

>  実際,MyModelでないModelとも協調動作するようなMyViewであれば,引数の
>型に対するcovariantは使わないでしょう.
>  しかし,MyViewがMyModelとしか協調動作できないのであれば,それは
>「もとの要求(すべてのModelと協調動作する)以下の仕事」しかしない
>わけですから,引数の型に対してcovariantを使えてもいいかもしれません.

こういうのは,静的型付けオブジェクト指向の限界というか,こういう場合にダ
ウンキャストが正当化される,という風に理解しています.この辺の事情という
のは,

・サブタイプと特殊化
・インターフェイス継承と実装継承
・クラス階層による分類とパッケージによる分類

という,ジレンマというか実際によく直面する問題をどう処理するかということ
に関わってくるんじゃないでしょうか.

でも,covarianceを使うのとダウンキャストを使うのとどちらがいいかを考える
と,部分的にLiskovを破ってでもcovariaceという気もしますね.polymorphicで
ないと困るメソッドに間違ってcovarianceを使うということも心配するほどなさ
そうな気がします.

C++な人はCircleをEllipseの子クラスにするのは間違っていると主張するでしょ
うが,Eiffelな人はそれは全く正しい,と考えるのかもしれません.そうなると,
フレームワークやクラスライブラリの設計に対する考え方も根本的に違っている
のかなあと思います.

                                     99/04/21(水) まさーる(BYI20012)

中間部分に出てくる Model と MyModel は,フレームワーク側のクラスとそれから継承してカスタマイズしたクラスのことを指しています.例えば,次のような Model-View を表すミニ・フレームワークがあるとします.

class Model
{
    ...
}

class View 
{
    public void setModel(Model model);
    public void Model getModel();
}

これを継承してカスタマイズされた MyModel, MyView クラスを作るのですが,大抵の場合次のようにしても問題ないでしょう.

class MyModel extends Model 
{
    ...
}

class MyView extends View
{
    public void setModel(MyModel model);
    public void MyModel getModel();
}

上記のプログラムでは, setModel の引数, getModel の返り値,ともに共変に変換されています.こうできれば,ダウンキャストがないわかりやすいプログラムになる,ということです.

参考資料