レベル: 初級 Eric Allen, Software Engineer, Cycorp, Inc.
2002年 6月 01日 Ruby、Python、そしてその他の、一般に普及しているいわゆる「スクリプト記述」言語の多くは、コードの信頼性の向上につながる一つの方法である静的型チェックからは遠い存在のものになっています。しかしやはり静的型チェックは、バグの潜入と検出に対抗する強力な武器庫の中の重要な武器の1つです。Eric Allenはこの記事で、静的型チェックへの支持を主張し、Java言語が静的型チェックをサポートすることがなぜ喜ぶべきことなのかを説明します。そしてどのようにすればそれをもっとよくできるかを検討します。
静的型 -- ほとんどのプログラマーは、これが好きか、あるいはこれが嫌いかのどちらかに入ります。支持者は、静的型を使うと、それを使わない場合よりもクリーンで信頼性の高いコードを生成できると自慢します。静的型をけなす人たちは、静的型によりプログラムに複雑さが加わると不満を言います。
確かに静的型を使うことで、若干代償は必要かもしれません。それを使おうとすると、時として退屈さをこらえることになります。しかし主な関心事がコードからバグを追い出すことであれば、全体として見ると、静的型を定義して使用するJavaのプログラミングはより好都合であるといえます。その理由は何でしょうか。静的型チェックで次のことができるからです。
- エラー検出を早いうちに行うことで頑健性を向上させる。
- 必要なチェックを最適なときに行うことでパフォーマンスを向上させる。
- 単体テストの不十分なところを補完する。
これらの理由を詳しく調べて、ペア・プログラミングと組合わせた静的型チェックを見てみましょう。
早期の検出で頑健性を向上させる
静的型チェックを行うとプログラムの頑健性が向上します。その理由は何でしょう。それを行うと、プログラムを実行する前に 可能な限り早い段階でエラーを見つけ出せるからです。これは、確実なことです。バグを特定するのが早ければ早いほど問題の診断が容易になり、誤った計算のために破壊されるデータ量が少なくなります。
プログラムを実行する前にバグを見つけ出して診断することは、望ましいと言えます。その優れた点を考えると、静的型チェックはプログラム言語の設計における大きな成果になります。なぜなら、静的型チェックはプログラムを実行する前にプログラムの中のバグを自動的に検出できる数少ない方法の1つであり、さらに許容できる時間内にそのチェックを実行できるからです。「許容できる時間」とは、時間がプログラムの長さの (定数係数が小さい) 1次関数であるということです。これに対して他の形式の自動チェックでは、3次や、さらには指数関数的な時間が必要になります (その多くは、そもそも完了することも保証されていません)。
型システムがもっと強力になれば、それをプログラムするのは容易になります (それが検出するエラーも多くなります)。Javaの型システムの単純さは、まだ改良が望まれる点をたくさん残していることは否定できません。その単純さはしばしば障害になり、キャストを使ってそれを回避せざるを得ません。しかしこの状況は、ゆっくりではありますが改善されています。
Sun JSR14コンパイラーでは、限定された形式の総称 (またはパラメーター化) 型が言語に追加されています。それは現在コミュニティー・プロセスでは確固たるものになっているので、それが言語に導入されるのは単に時間の問題であると確信できます。このJSR14に記載されている増加した表現力をもとに、NextGenなどの、より高機能の言語拡張がさらに構築される見込みです。多くの状況において、NextGenを使うと、JSR14でも追加されることが必要な複雑さのいくつかを軽減できるので、そのことは喜ばしいことです。このことについて詳しくは、『参考文献』のセクションを参照してください。(JSR14リンクのほかに、パラメタ的多相性 についての記事がいくつかあります。)
しかし静的型チェックから得られるものは、頑健性以外にもあります。プログラムのパフォーマンスも保護できるのです。
必要なチェックを減らしてパフォーマンスを高める
 |
NextGenへの道 ライス大学のプログラム言語テクノロジー・グループの1つが、NextGenと呼ばれる、GJの上位互換のバージョン用のコンパイラーをインプリメントしました。GJはMartin Oderskyによって作成されたコンパイラーです。ユーザーはこれを使用すると、既存のコードとの互換性を犠牲にしないで、Java言語に総称型 またはパラメーター化型 を追加することができます。その前身はPizzaと呼ばれていました。これはJava 1.2に導入されたいくつかの新機能を備えた実験的な言語でした。GJはPizzaに総称型だけを追加して改良されたものです。
GJはソースをバイトコードにコンパイルするのに型消去 を使用します。これは各型変数のあらゆるインスタンスを、その変数の上界に置き換える方法です。またGJでは、型変数をクラス全体用ではなく特定のメソッド用に宣言することができます。
NextGen言語は、ライス大学のRobert CartwrightとSun MicrosystemsのGuy Steeleが合同で開発したものです。この言語は、型変数の実行時のチェックを実行できる機能をGJに追加しています。この言語は個々のメソッドの型パラメーター化をサポートし、総称型での内部クラスと実行時操作をサポートしています。プリミティブ型を使って型変数をインスタンス化することはできません。
|
|
安全な言語 (「安全な」言語とはその言語独自の抽象化を壊せない言語のことです) では、アクセスされるフィールドの型のチェックはもちろん、メソッドに渡される引数の型のさまざまなチェックも必要であり、これらが実行されなければなりません。これらのチェックは、静的に行われないときは、実行時に行われなければなりません。
必要なこれらのチェックを実行するには時間がかかり、これらを実行時に行う言語ではそれに応じてパフォーマンスが犠牲になります。不変条件を静的にチェックすると、それを実行時にチェックする必要がなくなり、プログラムを高速にすることができます。したがって、静的型チェックを行うことは、より堅固なコードを生成できるということだけでなく、より効率的なコードを生成できることにもなります。
もともとコンパイル時の静的型チェックは非効率であると考えられています。各ファイルのさまざまな型参照を総合してリンクすると、C/C++ などの言語で書かれた大規模なプログラムでは長時間かかることがあります。これは、各コンパイルでさまざまなファイルが1つの大きな実行可能ファイルに結合されなければならないためです。しかしJava言語ではクラスは別々にコンパイルされてオンデマンドでJVMにロードされるため、その問題は完全に回避されています。すべての参照ファイルを1つの実行可能ファイルにリンクする必要がないため、そのようなコンパイル時の速度低下はありません。
ところで、単体テストのコンテキストでは静的型は不要であると主張する人たちには何を言いましょうか。
単体テストの限界を突破する
このコラムの愛読者ならご存知のように、私は単体テストを強く支持しています。単体テストでプログラムを保証することは最善の方法です。しかし私は初めて単体テストの限界を認めることになります。
単体テストは、特定の入力を使った特定の実行時のプログラムの振る舞いをテストするにすぎません。その実行の狭いコンテキストなら、そのプログラムの深い 性質をテストすることができます。
その一方、型チェックは浅い 性質をチェックしますが、可能性のあるすべての入力を使って、実行される可能性のあるプログラムのすべての動きについてチェックを行うのです。
型チェックと単体テストを結合させる
ちょうどストーリーと単体テストを相互に補完させてプログラムの振る舞いを明確にするように、単体テストと静的型チェックを相互に補完させてバグを特定して排除します。バグの除去の手段としてこれらの2つを結合させると、それらの個々の手段の効果を合計したものよりも大きな効果が得られます。
設計者やプログラマーの中には、洗練されたプログラムで起こるエラーの本質は、静的型チェックで見つかるものよりずっと深いところにあるということを言う人もいるでしょう。そしてその人たちは、静的型システムはその利用価値以上に邪魔になるという結論を出します。そして静的な型を持つ言語では、プログラムが冗長になったり、プログラムによっては決してエラーにならないプログラムでも書くことも妨げられることがあるのは、確かにそのとおりです。
トレーオフは常にあるものです。静的型を使用することはサービス・ランチではありません。静的な型がある言語で書くプログラムは、型システムのないもので書くプログラムよりもたいていは複雑になります。しかし「洗練された」プログラムでも、もっぱら静的型チェックで捕獲できる浅い種類のエラーがあるのです。
これらの浅いエラーをプログラム内で除去しても、リファクタリングを行えばそれらのエラーは簡単に再生されます。頻繁にリファクタリングを行うエクストリーム・プログラミングの考えを取り入れると、そのような浅いエラーはそれらが持ち込まれたらすぐに捕捉する必要があります。(その反対に、リファクタリングで起こる深いエラーは単体テストを行って捕捉します。この2つの概念を加えたときにもたらされる効果はそれぞれ単独でもたらされる効果を合計したものよりも大きくなります。)静的型チェックは、エクストリーム・プログラミングのコンテキストでも十分に効果があります。
型チェックと単体テストの間の矛盾
そうは言うものの、静的型チェックと単体テストの間の1つの矛盾について述べなければなりません。エクストリーム・プログラミングでは、単体テストを作成するときにその単体テストを実装するコードも書かなければなりません。
単体テストの各セットは、機能性の新しい性質を明確にすることができ、単体テストはそのテストの合否対象のコードを書く前に書く必要があります。理想を言えば、単体テストを書いたらすぐにそれをコンパイルして、いつでも単体テストができるようにしておきたいものです。
しかしここで問題があります。新規テストが参照するクラスとメソッドを定義するまで、その新規テストでは静的型チェックを行えないのです。これらのクラスとメソッドはあとで埋めるスタブにすることもできますが、環境が整っていないと、テストでそれらを参照しても静的チェッカーにとっては意味がないものになります。
次のリスト1の比較的簡単な例を見てみましょう。これはマルチセット (抽象データ構造) の実装のためのtestクラスを示したものです。
リスト1. マルチセットの実装のためのtestクラス
import junit.framework.*;
import java.io.*;
/**
* A test class for MultiSet.
*
*/
public class MultiSetTest extends TestCase {
private static String W = "w";
private static String X = "x";
private static String Y = "y";
private static String Z = "z";
private static MultiSet<String> EMPTY = new MultiSet<String>();
private static MultiSet<String> XY = new MultiSet<String>(X, Y);
private static MultiSet<String> YZ = new MultiSet<String>(Y, Z);
private static MultiSet<String> XYZ = new MultiSet<String>(X, Y, Z);
private static MultiSet<String> XYY = new MultiSet<String>(X, Y, Y);
private static MultiSet<String> WXY = new MultiSet<String>(W, X, Y);
/**
* Constructor.
* @param String name
*/
public MultiSetTest(String name) {
super(name);
}
/**
* Creates a test suite for JUnit to run.
* @return a test suite based on the methods in this class
*/
public static Test suite() {
return new TestSuite(MultiSetTest.class);
}
private void _assertOrder(MultiSet set, String key, int value) {
assertEquals("order for key " + key, value, set.order(key));
}
public void testEmpty() {
_assertOrder(EMPTY, X, 0);
_assertOrder(EMPTY, Y, 0);
_assertOrder(EMPTY, Z, 0);
}
public void testOrder() {
_assertOrder(XY, X, 1);
_assertOrder(XY, Y, 1);
_assertOrder(YZ, Y, 1);
_assertOrder(YZ, Z, 1);
}
public void testAdd() {
MultiSet added = XY.add(YZ);
_assertOrder(added, X, 1);
_assertOrder(added, Y, 2);
_assertOrder(added, Z, 1);
}
public void testSubset() {
assertTrue(XY.subset(XYZ));
assertTrue(YZ.subset(XYZ));
assertTrue(! YZ.subset(XY));
assertTrue(! XY.subset(YZ));
assertTrue(! XYZ.subset(XY));
assertTrue(! XYZ.subset(YZ));
assertTrue(! XYY.subset(XYZ));
assertTrue(! XYZ.subset(XYY));
}
public void testSubtract() {
MultiSet XYYZ = XY.add(YZ);
assertEquals(YZ, XYYZ.subtract(WXY));
assertEquals(YZ, XYYZ.subtract(XY));
assertEquals(XY, XYYZ.subtract(YZ));
assertEquals(EMPTY, EMPTY.subtract(YZ));
assertEquals(EMPTY, YZ.subtract(YZ));
}
public void testUnion() {
assertEquals(XYZ, XY.union(YZ));
}
public void testIsEmpty() {
assertTrue(EMPTY.isEmpty());
assertTrue(! XY.isEmpty());
}
}
|
クラスのMultiSet はどこにあるのでしょう。メソッドのunion() やisEmpty() などはどうでしょう。
型チェッカーはこれらのクラスとメソッドの位置について、読者の心当たり以上のことはわかりません。したがってこのコードは私の環境ではコンパイルできますが、読者の環境ではコンパイルされません。つまり、読者がクラスMultiSet の実装を、該当するすべてのメソッドを含めて書くまで、このコードはコンパイルされません。静的な型がある言語では、少なくともテストを行おうとしているクラスとメソッドのスタブを生成するまでは、新規の単体テストはコンパイルできないということを覚えておいてください。
この矛盾はテスト指向の開発ツールを使用することによって簡単に緩和できます。具体的には、単体テストを読み取れて、そのテストが静的型チェックをパスするのに必要なクラス参照とメソッド参照 (および該当するシグニチャー) を累積できて、そしてスタブ・クラスを生成できる開発ツールが必要です。
そのような開発ツールの設計がどのようなものであるかを考えてみると、エラーを生成する代わりにそれが生成するのに必要なスタブのログを単に累積することを除けば、テスト指向の開発ツールの計画は、まさに静的型チェッカーのようなものであることが非常に明確になります。私たちは、まさにこれを行う「スタブ生成」モードを備えたNextGen用の静的チェッカーを現在実装中です。
ペア・プログラミング: 別の浅いエラー・チェック
静的型チェックの、浅いが一般的なエラーの検出機能を補完するものとしては、この他にペア・プログラミング があります。これはエクストリーム・プログラミングの考え方の1つです。複数のインテリジェント・エージェントが相互に作業をチェックすることは、それらの浅いエラーの多くを除去するにはうってつけの方法です。
この効果を得るためのもう一つの有効な手段は、オープン・ソース・コードにすることです。コードをオープン・ソースにできると、頑健性が向上しやすくなります。とにかく、ちょっとした「分かった」を探しながらコードを調べるプログラマーの目を2組より多く持つことになるのですから。有名な "The Cathedral and the Bazaar" のEric Raymondが (Linusの法則と呼んでいるところで) 表現しているように、「目の球がたくさんあればバグはすべて浅い」のです。
単純な型チェックを超える
静的型チェックが役立つのと同じ理由で、さらに高機能な形式の静的チェックも、もちろん可能です。「静的チェック」と「静的分析」という用語は、単に型をチェックするということよりも一般的な概念です。これらはプログラムのテキストを分析してそのプログラムが実行時にどのように振る舞うかを判断するための何らかのメカニズムのことを指しています。
他のチームが紹介したように、Java言語を拡張して、表明の限定的な静的検査などの他の形式の静的チェックを組み込むことができます。今後の作業のもう一つの方向は、Java言語の上にさまざまな「ソフト型システム」を追加することです。それはどのようなものかというと、キャストなどの操作が特定のコンテキストでは成功することを検証しますが、一方、検証されていないキャストも禁止はされません。
バグを除去するには、できるだけ多くの不変条件をチェックできる新しい効果的な静的チェック・システムを開発することも含めて、この問題に専念するためのあらゆる手段を試みるべきです。今後のいくつかの記事では、Javaプログラミングに使用できる静的分析ツールのプロトタイプと実動ツールをいくつか検討します。
参考文献
著者について  | |  | Eric Allen氏は、コーネル大学でコンピューター・サイエンスと数学の学士号を取得しています。現在は、ライス大学の博士課程の大学院生としてJavaプログラミング言語チームに加わっています。学位を終了するためにライス大学に戻るまでは、Cycorp, IncでJavaソフトウェア開発主任として勤務していました。彼は、JavaWorldで「Java Beginner」ディスカッション・フォーラムの司会者も務めています。主な研究対象は、Java言語のセマンティック・モデルと静的分析ツールの開発であり、いずれもソース・レベルとバイトコード・レベルで研究しています。Ericは、NextGenというプログラミング言語 (汎用ランタイム型によるJava言語の拡張版) のためのライスのコンパイラーの開発にも携わってきました。連絡先は、eallen@cs.rice.edu です。 |
記事の評価
|