両方とも使えない:
- 逆変性: システムの妥当性エラーの理論的な問題はなくなるが非現実的
- 無変性: 強い型付き言語では(これを採用してしまうと)型システムが使い物にならなくなってしまう
- C++は無変性だが(型システムを壊し得る)キャストで回避
共変性と共存し得るアプローチを考える必要がある
総称性を使ったおもしろいアイディア:
- 共変性を持つ引数の型を総称パラメータとして定義する
- 顧客は、実際に使用したい型(ex. GIRL_ROOM)を実総称パラメータとして指定することで共変性の問題を解消可能
- この方法ではプロシージャは無変性となる
- 顧客は総称パラメータを通して共変性の必要性を満たすことになる
class SKIER1[G -> ROOM] feature
accommodation: G
accommodate (r: G) is ... require ... do accommodation := r end
end
--- 顧客コード (想像)
s: SKIER1[GIRL_ROOM]
g: GIRL1[GIRL_ROOM]
gr: GIRL_ROOM
br: BOY_ROOM
create g
create gr
create br
s := g
s.accommodate(gr) -- OK
s.accommodate(br) -- NG (compile error)
ただし、以下のような欠点があるため、一般的な解としては受け入れがたい:
- 可能な共変引数の型ごとにパラメータを一つ使う必要がある (総称パラメータのリストが無駄に膨大になる危険性)
- 新たな共変型をパラメータに持つルーチンを追加した場合に、全ての既存の顧客コードが無効になってしまう
- 共変性への対処法として型変数を言語に組み込むことを提案してきた人が何人かいる
- 適切に設計された型変数は総称性とアンカー宣言を包含し得る
- ただし:
- 後者二つは広く知られており分かりやすいが、カバー範囲の広い型変数は複雑で説明し難い
- 型変数を採用したとしても、それだけで(総称性およびアンカー宣言と同様に)共変性の問題を抜本的に解決することはできない
- どれを型変数にするかを決定するには、クラス設計者がそのクラスの使われ方を完璧に予測できている必要がある
- 以上の理由から、型変数ではなく既存のメカニズム(ex. 総称性、アンカー型、継承)上での(システム妥当性エラーに対する)対処方法を考えていくものとする
長いのでまず要約:
- アンカー宣言を使えば、コード量が少なくなるし、型チェックもより適切になる
- アンカー宣言便利
- ただし、これだけだとまだ型チェックの漏れ(抜け道がある)
- より制限を厳しくしたアンカーアプローチではどうか?
- 共変エンティティと多相的なエンティティを明確に分離する (両者の混在が問題の原因)
- システム妥当性エラーは防げるようになる
- ただし、オブジェクト指向の良さ(ex. 開放/閉鎖原則)まで制限してしまう
- 完璧な型チェックはあきらめて、現実的な落としどころを探っていこう (次節以降)
アンカー宣言を使えば共変性の問題にほとんど満足のいく解を実際に見出すことができる。
アンカーを使った例:
class SKIER feature
roommate: like Current
share (other: like Current) is ... require ... do
roommate := other
end
...
end -- クラス SKIER
class SKIER1 feature
accommodation: ROOM
accomodate (r: like accommodation) is ... require ... do
accommodation := r
end
end -- クラスSKIER1
上の例では:
- SKIER版は子孫で再定義が不要
- SKIER1版は属性accommodationを再定義しさえすればよい
- 残り(プロシージャの引数の型管理)はアンカーメカニズムが自動で行ってくれる
この驚くべきシンプルさを見れば、アンカーメカニズム(もしくは型変数といったこれに代わるメカニズム)なしには現実の型付きオブジェクト指向ソフトウェアを書くことは不可能であることが分かる。
ただし、これだけではシステム妥当性エラーはなくならない。
制限をより厳しくしなければ、まだ型チェッカをだますような多相的代入が可能。
--- 以下の不正なコードは防げるようになった
s: SKIER; b: BOY; g: GIRL
create b; create g
s := b -- 多相的代入
s.share(g) -- gは`like s`に適合しないため不正(compile error)
-- アンカー型の適合性規則(16.7.7)によれば、`like s`に適合する型は、それ自身のみ
--- ワンクッション挟んで、引数(g)にも多相的代入を行うようにすれば、型チェックをだませてしまう
s: SKIER; b: BOY; g: like s; actual_g: GIRL;
create b; create actual_g
s := actual_g; g := s -- sを介してgにGIRLオブジェクトをアタッチする
s := b -- 多相的代入
s.share(g) -- 型チェッカをパスしてしまう
(仮に)アンカー型エンティティを多相性で使うことを禁止するようにすれば、システムの妥当性エラーをなくすことはできる。
- anchor キーワード(架空)の導入
- 共変の属性には明示的な印(ahchor)をつけて、多相的なそれとは分離する
anchor s: SKIER
のように宣言された属性にのみlike s
が使える- 適合規則は従来のアンカーのそれをより厳しくした形で「型SKIERの任意のインスタンスからsへの代入 (多相 => 共変、代入)」が禁止されるようになる
- ルーチン引数の型の再定義機能は言語から削除 (anchor + like、で代替可能)
- 共変性-多相性の問題に対するこの解をアンカーアプローチと呼ぶことにする
アンカーアプローチのメリット:
- コンセプトが明確: 「潜在的に多相的なものから共変の要素だけを厳密に分離する」
- シンプル、エレガント、初心者に近しい人にも説明が容易
- 完璧に厳格: 共変性に関するシステム妥当性エラーの可能性をすべて排除可能
- 前々節で取り上げた総称性メカニズムとも組み合わせ可能
- 言語仕様は小さくしかも限られており、実装上の問題もなさそう
- 少なくとも理論的に現実的 (既存システムの置き換えが不可能ではない)
- コード的には、ほとんど共変の属性の前に
anchor
をつけるだけで済む - 代入に失敗する(コンパイルが通らなくなる)ケースもいくつかでてくるが、それらは「そもそも型違反」or「試行代入で置換可能」
- コード的には、ほとんど共変の属性の前に
アンカーアプローチは良さそうだが、なぜ採用しなかったか?
- 子孫隠蔽の問題は残っている
- 根本的な問題は、
- 「世界を多相的な部分と共変の部分に分けるヤルタ会談のような分割は、クラス設計者が常に完壁な予測を持っていることを前提にしている」
- 設計者は導入するすべてのエンティティに関して、それらが次の二つのどちらであるかを決定しなければならない
- 潜在的に多相的: 現在もしくは後になって、宣言された型以外の型のオブジェクトにアタッチされる可能性がある(故に再定義は不許可)
- 型を再定義されやすい: エンティティは アンカー型 or アンカー
オブジェクト指向技法の魅力の多くは「開放/閉鎖の原則」のようにオリジナルの選択を後に適用できるようにサポートされている点
- 汎用モジュールの設計者は永久的な知識が不要(であるべき)
- 不完全寛容型アプローチ
- 型再定義と子孫隠蔽の両方が安全弁となっており、それらのおかげで、ほぼ適した既存クラスの再利用が可能
- 型再定義:
- オリジナルをいじらずに、子孫で型宣言を適用させることができる
- アンカーアプローチでは、オリジナルコードの修正が必要
- 子孫隠蔽:
- 設計プロセスでの衝突に苦しまないで済む (いらない先祖のfeatureは、個の方が勝手に捨てれば良い)
- 全ての子孫のことを考慮して、(過剰なfeatureを含まないように)クラスを前もって設計するのは大変
- 24.7節「福型継承と子孫隠蔽」で分類学的例外について取り上げている
- 型再定義:
子孫適応の柔軟性を維持したいならば、共変性の再定義と子孫隠蔽を許す必要がある
アンカーメカニズムの基本的なアイディアは、共変部分と多相的部分を分けること。
システム妥当性エラーは、個々では妥当なO-Oメカニズムを不正に組み合わせた場合に発生し得る:
--- 例1: 共変性問題
s := b ... -- 多相的代入
s.share(g) -- 型の再定義がされていると齟齬が生じ得る
--- 例2: 子孫隠蔽問題
p := r ... -- 多相的代入
p.add_vertex(...) -- 子孫隠蔽がされていると齟齬が生じ得る
それぞれのエンティティの型集合(動的型の集合)を解析すればシステム妥当性エラーは検知可能:
- 型集合: 実行時にアタッチされることになるかもしれないすべてのオブジェクトの型からなる集合
- 例えば
POLYGON p; create {RECTANGLE} r; p := r;
という命令群があった場合、pの型集合にはRECTANGLEが含まれるようになり、p.add_vertex(...)
のような呼び出しは不正なものとして扱われるようになる
グローバルアプローチの型規則:
システム妥当性規則 (System Validity rule) 呼び出し x.f(arg) において、それ自身の型集合の任意の型を持つ x と、それ自身の型集合の任意の型を持つ arg に対し、クラス妥当でありさえすれば、その呼び出しはシステム妥当である。
型集合の決定ルール:
- 省略(T1 - T5): 要は代入とか実引数の適用とかがあるたびに、そのエンティティ型集合に型を追加していきます、という話
- コンパイラが大変(フロー分析)なので、命令の順序は考慮しない
create {TYPE1} t; s := t; create {TYPE2} t;
なら、sの型集合にはTYPE1とTYPE2の両方が含まれる- 結果として悲観的な妥当性チェック
グローバルアプローチなら、共変の問題と子孫隠蔽の問題の両方が解決可能。
ただし「悩みの種となる現実的な欠陥」があるため、実際には採用が難しい:
- クラスを1つずつではなく、 システム全体 を一度にチェックすることが前提
- 規則T4(関数の仮引数パラメータの型集合周り)が特に危ない
- ex. 汎用ライブラリの関数fがあるとして、新規顧客がfを呼び出すと、他の既存顧客にも影響がおよび可能性がある
- 感想: 例えば、既にコンパイルが通っている別々のライブラリを、一つのシステムで一緒にビルドするようにしただけで、ライブラリ側にエラーが発生し得るのは結構恐い (ライブラリ単体でのコンパイルが通っても安心できない)
- グローバルな分析フェーズが必要となり、インクリメンタルな差分コンパイルの恩恵が受け難くなる (コンパイル時間の問題)
- グローバルアプローチの規則は教えにくい (総称性などの詳細すべてが関わってくる場合は特に)
結論:
- クラス妥当性は、エンティティのそのクラスの静的型宣言だけを参照すれば良く、インクリメンタル(ローカル)にチェック可能
- システム妥当性は、エンティティの実行時(に取り得る)型情報を考慮する必要があり、システム全体(グローバル)の分析が必要
- 「完全なインクリメンタルチェックでは、システム妥当エラーをなくすことは実際不可能である。そして、これがこの問題に対する最終結論である」
グローバル分析のシステム妥当規則は悲観的:
- 型規則やその強要を単純化したことで、無害な組み合わせも排除されてしまうかもしれない
- 最終的な解は、規則をもっと悲観的(Catcall解)にすることで得られる
- その結果どう現実的になったかをこの節で説明
Catcall解:
- アンカー解のヤルタ的な性質に戻る
- ヤルタ的性質: 世界を多相的な部分と共変の部分に分割する (+ サテライトとして子孫隠蔽の部分)
- 根本的な問題は、互換性のない2つの方法で、エンティティを使おうとしたこと
- 「多相的エンティティ + 共変ルーチンへの呼び出し先」 or 「多相的エンティティ + 子孫隠蔽ルーチンの呼び出し先」
- SKIERの例でいえば、sが多相的に代入されたSKIERクラスである限りは、共変ルーチンshareを適切に呼び出すことは不可能
- Catcall解はアンカー解と同列にあるが、より強烈
- 同じエンティティを多相的と共変的の両方に使うことを禁止
- グローバル解のように型集合を探し出すことはせずに、共変性や子孫隠蔽の構築を阻む疑いのあるものとして多相的エンティティを扱うだけ
Catcall型規則:
多相的Catcallは間違いである
定義: 多相的エンティティ
(拡張されていない)参照型のエンティティxは、次の性質のいずれかを満たす場合に、多層的となる。
P1: 代入 x:= y にあって、yが別の型または(再帰的に)多層的である場合
P2: 生成命令 create {OTHER_TYPE} x で、OTHER_TYPEが宣言されたxの型ではない場合
P3: ルーチンの仮引数の場合
P4: 外部ファンクションの場合 (?)
この定義により「実行時に1つ以上のオブジェクトにアタッチされる可能性のあるエンティティ」は(潜在的に)多相的なものとして扱われる。
多相的エンティティの概念は、グローバルアプローチの型集合の概念よりも悲観的(かつチェックがより単純):
- エンティティの可能な動的型すべてを見つける代わりに、多相的になり得るか否かというバイナリ的性質にとどめている
- もっとも際立っているのは「ルーチンの仮引数はすべて多相的である」と見なす点
- 引数に何を渡すかは顧客の自由 (クラス妥当な限りは)
- 全て多相的と見なしてルーチンを実装すれば、どんな顧客ソフトウェアでも、そのルーチンが呼び出し可能になる
- この規則により、クラスはすべて、永久的に再利用可能なライブラリの一部となる潜在的能力を有する
- グローバルチェック不要 (インクリメンタルチェック可能)
エンティティ同様、呼び出しは多相的である可能性がある:
定義: 多相的呼び出し
呼び出し先が多相的である場合、その呼び出しは多相的である
CAT(Changing Availability or Type)の定義:
定義: CAT(有効性を変更するか、もしくは型を変更するか)
あるルーチンを再定義する際、その公開情報または引数のいずれかの型が変更される場合、そのルーチンはCATであるとする
この性質もインクリメンタルチェック可能。
Catcall:
定義: Catcall
公開情報もしくは引数の型変更により、あるルーチンの再定義が違反となる場合、この呼び出しをCatcallという
※ この定義はあまりしっくりこない
Catcall型規則は、呼び出しを多相的呼び出しとCatcallの2つの別個のカテゴリに分けることになり、ヤルタ的な面を強化する:
- 多相的な呼び出しはO-O技法の表現力を生み出す: 有益性(usefulnes)の強化
- Catcallは型を再定義する能力と特性を隠蔽する能力を生み出す: 利用性(usability)の強化
SKIERやPOLYGONの例はどちらも多相的Catcallにあたり、Catcall型規則により誤りとなる。
疑問:
- あるルーチンがCATであるかどうかを、その子孫での定義によって決めるなら、汎用的なライブラリクラスが実装し難くはないか?
- Aを継承するクラスBを追加してその中で再定義等を行ったら、その瞬間にAを参照しているすべてのコードの多相的呼び出しが誤りになる?
- 安全策を取るなら、すべてのルーチンは、仮引数として受け取ったオブジェクトのルーチンを(将来的に再定義される可能性があるため)一切呼び出せなくなるのでは?
- 全てのクラスの情報が揃っているという仮定のもと(ライブラリではなくアプリケーションの実装)でなら、特に問題なさそう。
リマインド:
- システム妥当性違反は極端にまれにしか起きない
- 静的なO-O型付けの最も重要な性質
- クラスレベルの妥当性が高くなった
- ソフトウェア構築の安全で柔軟な方法への道が開かれたこと
共変性問題(および子孫隠蔽)に関するこれまでの解のいずれが正しいか?
- 答えはまだ出ていない
- グローバル解: システム全体のチェックが前提のため非現実的だが、問題理解の助けにはなる
- アンカー解: シンプルで実装しやすいが、極端に仮定的(設計者が正確な予測ができる、という)で開放/閉鎖原則等を満たせない
- Catcall解: 一番妥当っぽい
- 説明も強要しやすい
- 悲観的な要素はあるが、有益な組み合わせは排除していない
- 多相的Catcallが合法に見えるケースは、試行代入で代替可能
- 注意: 執筆時点では、まだCatcall解の実装はない
共有の議論の補足として、共有の問題を記述する一般的な技術を学ぶことは有益である。この技術はCatcall理論の結果として出来たものであるが、新しく規則を設けなくても、基本的な言語のフレームワークで使うことができる。
GENERAL.same_type
を使って、実行時に型の一致チェックができる- 引数は無理に共変にしない (SKIERの例なら子孫でも SKIER のままにしておく)
- ルーチン内で、引数と自分の型一致チェックをして、一致するなら処理を進めて、不一致ならエラーを返す
- こういった方法があるので、多相的Catcallを制限しても実際上の問題はない
省略
省略