以下の効果が、静的型付け(typing)によってソフトウェアにもたらす
- 信頼性の向上
- システムがダメージを受けてしまう前にコンパイラなどのツール群で食い違いが起きないようにする
- 可読性の向上
- 顧客システムの作成者や開発しているソフトウェアの未来の管理者など、プログラムを読む人たちに正確な情報を提供する
- 生産性の向上
- 型情報のおかげで優れたコンパイラは効率の良いコードを生成することができる
ただ、静的型付けによりいくつかの問題が生じる。 この章では、それらの未解決な問題の解を探っていこう。
- オブジェクト指向システムの実行中に起きるイベントは一般形式の特性呼び出し1種類だけである。
x.f(arg)
- この基本的構成概念から実行時に起こりうる異常状態が導き出せる
【定義: 型違反(type violation)】
xが、オブジェクトOBJにアタッチされており、以下のどちらかの条件にあるとき、呼び出しx.f(arg)の実行において実行時違反(run-time type violation)が発生する
* V1
- fにあたる特性が存在せずOBJに適用できない
* V2
- fにあたる特性は存在するものの、argが正しい引数ではない
型違反を避けるためには、次の型付けの問題を解決する必要がある。
【オブジェクト指向の型付問題】
あるオブジェクト指向システムを実行したら型違反が生じるかどうかは**いつ**分かるのか
キーワードはいつであり、それはは「いますぐ」か「後になって」のいずれかである。 問題は早くわかることに越したことはない。
型付けには大きく2つアプローチがある。
- 動的型付け
- 最後の可能な瞬間、つまりそれぞれの呼び出しが実行されるまで待つ。実行時に型を検証する
- 静的型付け
- ある規則郡に従い、システムのテキストを基に、そのシステムを実行すると型違反が起きるかどうかを決める。 そして、それらの規則により違反が起きないと保証されるシステムだけを実行する(静的とは、実行前という意味である)。
「型付き(typed)」や「型なし(untyped)」という用語が「静的型付き」や「動的型付き」に対して使われる場合がある。混乱を避けるために、ここではフルネームを使うことにする。
【定義: 静的片付け言語】
あるオブジェクト指向言語が整合性規則郡を備え、コンパイラによってそれを強制している場合、その言語は静的型付け言語である。
その際、システムテキストに整合性規則を守らせることで、そのシステムを実行しても型違反は起き得ないと保証される。
- すべてのエンティティやファンクションは特定の型のものとして宣言されなければならない
- 代入
x := y
もしくは、仮引数xに対する実引数にyを用いたすべてのルーチンコールにおいて、ソースyの型はターゲットxに適合しなければならない x.f(arg)
形式の呼び出しにおいて、fはxの型の基本クラスの特性であり、呼び出し元のクラスで利用可能でなければならない
- TODO:
- TODO:
静的型付けの議論に使われる用語をおさらいする。
- 静的型付け言語を使う理由は、「信頼性」、 「可読性」、「生産性」である。
実行時にだけ現れるエラーや特定の実行でのみ生じるエラーを発見することができれば、信頼性は評価される。 エラーの早期発見は、プロジェクトにおいて修正コストを小さくする効果がある(逆に発見が遅れるほど修正コストが高くなる)。
すべてのエンティティやファンクションを特定の型で宣言することは、そのソフトウェアを読むものに利用目的についての情報を伝えるパワフルな方法である。 可読性を高めるということはソフトウェアメンテナンスにおいて重要なことである。
もし、可読性が目的の一部でないならば、明示的な宣言をしなくても、別の型付の利益をえることができるかもしれない。特定の条件下であれば、型付けの暗黙的な形式(ソフトウェアの作成者がエンティティの型を宣言する代わりに、コンパイラが、その使い方から自動的に各エンティティの型を推定するといったやり方)を取ることも確かに可能である。これは、**型推論(type inference)**として知られる方法である。しかし、ソフトウェア工学的観点から見れば、明示的な宣言はペナルティではなく救済であり、型は、コンパイラだけではなく、それを読む人にも明確にすべきものである。
静的型付けがなければ、x.f(arg)
の実行にどれくらい時間がかかるかあいまいになってしまう。
特性f
の探索効率はアルゴリズムを改善することで軽減され得るが、オブジェクト指向ソフトウェアが生産性において匹敵したのは静的型付けによって、
探索する型の集合を限定させ、コンパイラは実行時に正しいf
を最小の(かつ一定に制限された)コストで生成できることにある。
静的型付けの利点にもかからわず、動的型付けを支持する人たちはいる。 支持する論拠として現実主義的見解にあり、静的型付けは制限し過ぎであり、ソフトウェアのアイデアを表現する自由を妨げるという主張である。
これまでの章で説明してきた機能のおさらい。
- 型システムはクラスの概念にもとづいている(基本型もクラスによって定義される)
- 拡張型により型の値がオブジェクトの参照だけではなくオブジェクトそのものを表すことができ柔軟性が高い
- 継承と適合性の概念の導入。
x := y
において、xとyの型が同じという規則は厳しすぎる。yの型がxの型に適合すれば良い - 多重継承をサポートすることでオブジェクトに対して必要な数の視点を取り入れることが可能である
- 柔軟で型安全を保つコンテナデータ構造を定義するためには、総称性が必要
- 総称性によっては、総称型のエンティティに特定の操作を適用するように制限が必要
class SORTABLE_LIST[G -> COMPARABLE]
- 試行代入によって、オブジェクトに対して型安全に操作可能である
x ?= y
- 表明により型使用では表し得ない意味的な制約を表すことができる
- アンカー宣言は再宣言の嵐をさけるのに実際かかせないものである
- アンカー宣言は共変性の特殊なケースである
- 迅速な繰り返し型リコンパイル(fast incremental recompilation)は、最後にコンパイルされてから何が変更されたかを感知し、その部分だけ処理しなおすので、リコンパイル時間は小さいまま(システムの大きさではなく変更の大きさに比例する)である。
静的型付け言語での抜け穴として、エンティティの型を偽るような変換がある。Cやその仲間においてその変換は「キャスト(casts)」と呼ばれ簡単な構文に従う。
例えば、(OTHER_TYPE)x
はxの値が型OTHER_TYPE
であるかのようにコンパイラに見せることを示す。
しかし、開発者が好きなときにキャストによって型規則を避ける事ができるのであれば、静的型付けの主張は受け入れがたいように思う。
したがって、これ章では、以降型システムは厳密でありキャストは許されないものとする。
現実の型システムの必要不可欠な成分として先に述べた試行代入は、表面的にキャストに似ているという人もいるかもしれない。しかし、根本的なところが間違っている。つまり、試行代入の方は闇雲に違った型にしてしまうわけではない、候補となる型を試し、オブジェクトが実際にその型に適合するかどうかをソフトウェアにチェックさせている。これは安全であると同時に、ある種の環境では不可欠な機能でもある。C++の文献によっては、キャストの定義に試行代入(ダウンキャスト)を含むものがある。この場合、明らかに、キャストを禁止すれば、有害なバリエーションだけでなく試行代入の拡張機能もなくしてしまうことになる。
静的型付け(typing)と静的束縛(binding)の概念を混同してしまう人もいる。
Smalltalkの影響によるかもしれない。Smalltalkは、型付けと束縛の両方に動的アプローチを用いているため、いい加減に見ている人には、どちらの問題にも同じ解でなければならないと間違った印象を持ってしまう人がいる。
【型付けと束縛】
* 型付けの質問: 実行時にfにあたる操作が存在し、xにアタッチされたオブジェクトに適用可能(引数argを使って)であることが確かにわかるのはいつか
* 束縛の質問: どの操作がその呼び出しで実行されるのか
型付けは少なくとも1つの操作の存在を示し、1つ以上の候補が存在する場合に、束縛はそれらの操作の中の正しい1つの選択を表す。 どちらも実行中を意味する「動的」と実行前を意味する静的にあたる解があり得る(静的型付け・動的型付け、静的束縛・動的束縛)。 実際の言語には、4つの可能な組み合わせのすべてが存在する。
- 動的型付け+静的束縛: Pascal、Ada
- 動的型付け+動的束縛: 信頼性と引き換えに柔軟性を選ぶ
- 型なし(動的型付けを意味する) + 静的束縛: アセンブラやスクリプト言語
- 静的型付け+動的束縛: Eiffel!
静的型付けと動的束縛を選ぶ理由は明らかである。
- できるだけ早期にエラーを発見する(静的型付け)
- そのオブジェクトの型に直接適合する正しい特性を選択する(動的束縛)
ためである。
世界が単純ならば、型付けの議論はここで終了する。が、世界は単純ではない。 共変性(再定義における引数の型変更)と子孫隠蔽(クラスにおける継承された特性の公開制限能力)が問題を難しくする。
※しばらくアンカーを忘れることにして別の例を元に再考察してみる。
class SKIER feature
roommate: SKIER
share(other: SKIER) is
require
other /= Void
do
roommate := other
end
end
class GIRL inherit
SKIER
redefine roommate end
feature
roommate: GIRL
share(other: GIRL) is
require
other /= Void
do
roommate := other
end
end
真の子孫はすべてこのようにして適合させなければならない。 継承は特化に当たるため、特性を再定義する場合新しい型は必ずもとの型の子孫でなければならないという型規則が必要である。 このポリシーは共変性と呼ばれる。
プロシージャは普通共変である(同時にアンカー型を使うのが普通である)
NOTE: 並行階層とは、移譲しているクラスにも階層があること?
class SKIER1 feature
accomodation: ROOM
accommodate(r: ROOM) is ... require ... do
roommate := other
end
end
SKIER1を継承する各クラスでそれぞれ共変の再定義をする必要がある。
共変のポリシーで多相性が引き起こすかもしれない問題について考えてみる。
s: SKIER, b: BOY, g: GIRL
create b; createg;
s := b; -- 多相的代入
s.share(g)
share
はクラスSKIERの公開特性でり、引数gの型であるGIRLはSKIERに適合するので、この呼出は型適合となる。
子孫隠蔽(descendant hiding)とは、親の1つによって公開された特性を、あるクラスで非公開とする能力である。
- クラス妥当(class-valid): すべてのエンティティは型が宣言され、すべての代入や実-仮引数の関係は型の正当性を満たし、すべての呼び出しには、呼び出し元に公開された呼び出し先の型の特性が使われている
- システム妥当(system-valid): 実行時にに型違反が起き得ない
共変性と子孫隠蔽を使ったシステムはクラス妥当ではあってもシステム妥当ではなくなってしまう。そのようなエラーはシステム妥当性エラー(system validity error)
実際の開発においては、システム妥当性エラーは極端に稀である。 一報、クラス妥当性エラーは日常的に発生する(コンパイラによって発見される)。 だからといって、システム妥当性エラーを無視していいわけではない。