レベル: 初級 Bruce Tate (bruce.tate@j2life.com), President, RapidRed
2006年 3月 25日 型定義の扱いについて、Java™コミュニティーの意見は分かれています。一部の人達は、静的な型定義によって可能となる機能、つまりコンパイル時でのエラー・チェックや高いセキュリティー、改善されたツールを好んでいます。また一方、より動的な型定義を好む人達もいます。境界を越えるシリーズの今回は、非常に生産的な(Javaではない)2つの言語による、劇的に異なる型定義戦略について調べ、またJavaプログラミングにおいて柔軟な型定義を実現するための方法について検証します。
プログラミング言語に関する議論の中で、型定義のモデルは最も意見が分かれる問題の1つであり、それにはもっともな理由があります。型定義によってツールの種類が決まり、アプリケーションの設計が影響を受けます。多くの開発者は型定義を、(私と同じように)生産性の面から考えるか、あるいは維持管理の容易性から考えます。典型的なJava開発者はJava言語の型定義モデルを守ろうとしがちであり、その根拠として、開発ツールが優れている、ある種のバグ(型の不一致やスペルミスなど)をコンパイル時に捉えられる、パフォーマンスが優れている、といった理由を挙げます。
皆さんが新しいプログラミング言語、あるいはプログラミング言語ファミリーを理解しようとする際には、型定義の戦略から始める場合が多いものです。この記事では、Javaの型定義モデルに代わりうる方法について学びます。最初に、言語設計者が型定義モデルの中で考慮すべき判断事項について、議論になりがちな静的型定義と動的型定義に焦点を当てながら一般的な説明を行います。そのために、このスペクトルの両端にある例、つまり静的型定義としてObjective Camlを、また動的型定義としてRubyを示します。またJava言語での型定義の制約を説明しながら、その制約の範囲内でも、またその制約に逆らっても、生産的にプログラミングが行えることを説明します。
 |
このシリーズについて
この、『境界を越える』シリーズでは、著者であるBruce Tateが、「今日のJavaプログラマーは、他の手法や言語を学ぶことから多くを得ることができる」という概念を押し進めます。プログラミングの世界の様相は、あらゆる開発プロジェクトにとってJavaを選択することが明確に最善であった頃から変わってきています。他のフレームワークもJavaフレームワークと同じ構築形態をとりつつあり、また他の言語での概念を学ぶことによって、それをJavaプログラミングに生かすこともできます。皆さんが書くPython(あるいはRubyやSmalltalk、その他何であれ)コードによって、皆さんのJavaコーディングに対する取り組みも変わる可能性があるのです。
このシリーズでは、Java開発とは大幅に異なりながら、同時にJava開発に直接応用できるプログラミング概念や手法を紹介します。場合によると、こうした技術を利用するためには統合が必要かも知れません。また場合によると、ここで紹介する概念を直接応用することもできます。他の言語やフレームワークが、開発者やフレームワーク、Javaコミュニティーでの基本的な手法にまで与えうる影響に比べると、個々のツールはそれほど重要ではありません。
|
|
型定義に関する戦略
型定義は、少なくとも次の3本の軸に沿って考えることができます。
-
静的型定義と動的型定義という区別は、『いつ』型定義モデルを強制するかに依存します。静的型定義の言語では、コンパイル時に型定義を強制します。動的型定義の言語では、通常はオブジェクトの特性に基づいて、実行時に型定義を強制します。
-
強い型定義と弱い型定義という区別は、『どのように』型定義モデルを強制するかに依存します。強い型定義では積極的に型を強制し、型定義のルールを破るとランタイム・エラーあるいはコンパイル・エラーが投げられます。弱い型定義では、もっと緩やかです。アセンブラーは弱い型定義の究極の姿として、任意のデータ型を、任意の他の何かに割り当てることができます(その割り当てに意味があるか否かは無関係です)。静的な型定義の言語では、強い型定義、弱い型定義のいずれも可能ですが、動的な型定義のシステムでは、(必ずではありませんが)通常は強い型定義が使われます。
-
明示的型定義(『マニフェスト型定義』とも言われます)と推論(inferred)型定義という区別は、対象とするオブジェクトの型を言語がどのように判断するかに依存します。マニフェスト型定義の言語では、各変数と、関数の引数それぞれを宣言するように強制されます。推論型の言語では、そのオブジェクトが言語の構文的なヒントに基づくのか、あるいは構造的なヒントに基づくのかが判断されます。静的な型定義の言語では、(必ずではありませんが)通常は明示的な型定義を使いますが、動的な型定義の言語では、ほとんど常に推論型定義が使われます。
次の2つの例を見ると、この3本の軸のうちの2本で何が起こるかがよく分かります。まず、下記のJavaコードをコンパイルすることを考えてみてください。
class Test {
public static void test(int i) {
String s = i;
}
}
|
そうすると、次のようなエラー・メッセージが出ます。
Test.java:3: incompatible types
found : int
required: java.lang.String
String s = i;
^
1 error
|
また、下記のRubyコードを実行することを考えてみてください。
そうすると、次のようなエラー・メッセージが出ます。
TypeError: String can't be coerced into Fixnum
from (irb):3:in '+'
from (irb):3
|
意図した型構造外でオブジェクトを使おうとすると、どちらの言語もエラー・メッセージを投げることから、両者は型定義が強い傾向があります。Javaの型定義戦略では、静的な型チェックが行われるため、エラー・メッセージはコンパイル時に投げられます。Rubyは動的な型定義をサポートしているため、エラーは実行時に起こります。別の言い方をすると、Javaはコンパイル時にオブジェクトを型にバインドします。一方Rubyは、オブジェクトが変更される毎に、実行時にオブジェクトを型にバインドします。私はJavaコードでは変数を宣言しましたがRubyでは宣言しなかったため、Java言語での明示的な型定義とRubyでの推論型定義との違いの実際が見えたのです。
上記の3本の軸のうち、言語の特性に最も影響を与えるのは、静的型定義と動的型定義との違いです。そこで今度は、この2つの戦略それぞれの強みを見ることにしましょう。
静的型定義の強み
静的型定義の言語では、プログラマーは(宣言によって、あるいは習慣的に)、あるいはコンパイラーは(構造的、構文的ヒントに基づいて)、変数またはオブジェクトに型を割り当てます。そして、その型は変わりません。通常、静的な型定義にはコストがかかることが多いものです。これは、静的型定義の言語(Javaなど)は明示的に型定義を行うためです。つまり、すべての変数を宣言する必要があり、コードをコンパイルする必要がある、ということです。ただしコストの見返りとして、早い段階でエラーを検出することができます。最も基本的なレベルとして、より多くの情報をコンパイラーに与えるために静的型定義が存在しています。こうした追加情報の利点として、動的型定義の言語では実行時まで検出できないような、ある種のエラーを捉えられることできます。もしこうした種類のバグを、実行時に検出するまで放っておくと、その幾つかは実稼働の中に入り込んでしまうかも知れません。この点こそ、動的型定義の言語に比較して優れている点と言えるでしょう。
その反論として、最近のソフトウェア開発チームは自動テストを実行する場合が多い、という現実があります。そして動的型定義を支持する人達は、ごく単純な自動テストでも大部分の型定義エラーは検出できる、と主張します。しかし、コンパイル時のエラー検出という利点に対抗して動的言語の支持者が行える最大の反論は、(動的型定義を使うか否かによらず)いずれにせよテストは行う必要があるため、早期検出はコストに見合わない、という点でしょう。
その釣り合いをとるための興味深い方法として、静的型定義言語で推論型定義を使って型定義のコストを下げる、という方法があります。オープンソースのOCmal(Objective Caml)は、Lispから派生した静的型定義の言語であり、生産性を犠牲にすることなく素晴らしいパフォーマンスを発揮します。OCamlは推論型定義を使用しているため、次のような静的型定義が可能です。
これに対してOCamlは下記を返します。
OCamlは、この表現の中にある構文的なヒントに基づいて、「x. 4の型がintであり、7がint、よってxもintであるはず」と推論します。推論型定義言語は、Java言語の持つタイプセーフと、さらにそれ以上のものを備えています。Javaとの違いは、提供すべき情報量と、プログラムを読む際に得られる情報量の違いです。静的型定義を好む人の多くは、推論型定義を好みます。そうした人達は、自分で繰り返しコードをいじるよりも、コンパイラーに作業をさせたがるのです。
推論型定義の大きな利点は、渡されるパラメーターからコンパイラーが推論してくれるため、関数に対する引数の型を宣言する必要がないことです。このため、同じメソッドを複数の目的に使うことができます。
 |
リファクタリングに対する誤解
良質なIDEでリファクタリングをサポートするためには静的型定義が『必須』と考えるのは誤解です。最近のほとんどのIDEでは、初期のSmalltalk IDEの概念を、少なくとも一部は採り入れています。実際、Eclipseの初期のルーツをたどると、Visual Age for Javaに突き当たります。これはSmalltalk仮想マシンに同梱されていたものです。今でも、Smalltalk Refactoring Browserは、入手可能なツールの中で最も完全な機能を持ったものです(参考文献を見てください)。とはいえJava言語には、(Smalltalkを除けば)大部分の一般的な動的言語よりも良いツールがあり、それは何と言っても静的型定義であるためなのです。
|
|
静的型定義で提供される追加的な情報を利用できるのは、コンパイラーだけではありません。例えばIDEは、静的型定義によって、より適切にリファクタリングを行うことができます。数年前に登場した革命的な概念によって、開発環境の動作方法が大きく変わりました。IDEAやEclipseでは、コードはテキスト・ビューのように見えますが、開発環境は実際にはAST(Abstract Syntax Tree)を編集しています。そのため、例えばメソッドあるいはクラスをリネームしようとする場合、ASTでの位置を指せば、環境はメソッドやクラスが使われている全ての場所を容易に見つけることができます。今日では、適切なリファクタリングを行わずにJava言語のプログラミングを行う事は困難ですが、静的型定義によってリファクタリングが容易になるのです。私がRubyを試す中で一番残念なのは、他のどのツールや機能でもなく、IDEAが無いことです。
ここでは詳細に説明しませんが、静的型定義には他にも幾つかの利点があります。静的型定義によってセキュリティーを改善できる可能性があり、また確実にコードの読みやすさを改善することができます。また静的型定義では、より多くの情報がコンパイラーに提供されるため、コンパイラーは早い段階で最適化を行うことができ、またパフォーマンスを改善することができます。しかし大部分の開発者にとって静的型定義の最大の魅力は、何と言ってもエラーが初期に発見できること、そして良いツールが揃っていることでしょう。
動的型定義の強み
Rubyの精通者であるDave Thomasは、動的型定義に『アヒル型定義(duck typing、参考文献を見てください)』というレッテルを貼りましたが、これには2つの理由があります。第1は、Ruby言語は実際には型定義を実装していない、つまり問題を避けている(この場合のduckは「問題を避ける」の意)という点です。2番目の理由は、「もし何かがアヒルのように歩き、アヒルのような鳴き声をたてるのであれば、それは恐らくアヒルであろう」という考え方からです。プログラミング言語という意味合いでアヒル型定義を考えると、もし、ある型のメソッドに対してオブジェクトが反応するのであれば、現実的な意味から考えて、そのオブジェクトをその型であるかのように扱うことができる、ということを意味します。この振る舞いから、興味深い最適化を実現することができます。
動的型定義を好む開発者の大部分は、その理由として、初期段階でのエラー検出コストがテストによって不要になることの他に、動的型定義言語の持つ表現力と生産性を挙げます。非常に単純に言うと、より少ないキーワードを使って、より多くの概念を表現できるのです。私は新たにRubyに改宗した者として、動的言語はより生産的であると固く信じていますが、通常の静的言語支持者に比べて強力な証拠を持っているわけではありません。しかし私は、どんどんRubyでコードを書くようになってから、実際に生産性の改善を経験しています。確かに、(特にツール・セットでは)静的型定義の利点は相変わらず認めますが、欠点にも気がつくようになりました。
私にとって、Rubyでコーディングするようになって大きく変わったことは、メタプログラミング構成体を生成、利用できるようになったことです。『境界を越える』シリーズを最初から読んでいる人であれば分かると思いますが、メタプログラミング(つまりプログラムを書くプログラム)は、Ruby on Railsを筆頭とした、ドメイン専用言語を推進する原動力の1つです。Rubyでは、通常は大きな構成ブロックをコーディングします(つまり、大きなブロックを構成します)。そしてJavaでコーディングする場合に比べ、より多くの種類の再利用可能ブロックを使ってプログラムを拡張できることに気がつきます。またJavaでのプログラミングと同様、新しいクラスでプログラムを拡張することもできます。また、クラスはオープンなため、既存のRubyクラスに対してメソッドやデータを追加することもできます。ミックス・イン(mix-in、ランタイム・バインディングの項を見てください)を使って、既存のクラスにコア機能を追加することができます。また、目的に合うように、いつでもオブジェクトの定義を変更することができます。私は初心者のRubyプログラマーなので、こうした機能が頻繁に必要なわけではありませんが、実際に機能を使い始めてみると、その結果に驚かされるのです。
例えば、インターセプターを追加するためには、単純にメソッドをリネームし、オリジナル・メソッドの新しい実装を作成します。例えばnewをインターセプトするためには、下記のようなコードを書きます。
class Class
alias_method :old_new, :new
def new(*args)
puts "Intercepted new" #do interception work here
old_new(*args)
end
end
|
AspectJライブラリーやバイトコード・エンハンスメント、ライブラリー・スタックなどは必要ありません。必要な場所で直接インターセプターをコード化すればよいのです。
さらに別の視点からの評価として、動的型定義によって、生のコード・ライン数の面からも作業量を減らすことができます。動的言語は、ほとんど常に推論型定義なので、基本的な概念を表現するために大層な努力は必要ありません。変数を宣言する代わりに単純に変数を使い始めればよく、また引数の型の順列をすべて表現する代わりに、単に名前のリストを入力すればよいのです。コードは、より多形的(polymorphic)になります。つまり、あるメソッドに反応するものは、すべて1つの型として扱われます。ですから多くの場合、他の言語よりも簡潔に概念を表現することができます。また、コード内の結合も、ずっと少なくなります。もし何かの型を変更したい場合でも、通常ローカルな変更で済み、複数の場所で変更を行う必要がありません。
 |
安全性と柔軟性
静的言語か動的言語かという議論の要点は、ある意味で安全性対柔軟性か、という点に尽きます。静的言語の支持者達は、より安全な言語が良いのだと信じています。動的言語の支持者達は、安全性のために費やされる労力を嫌います。彼らにとって言語を評価するための鍵は、プログラマーの効率を最大にするためにいかに素早く概念を表現できるか、という点です。そうしたスペクトル分布の対極として、静的言語のエキスパートは、初期のバグを捉えることが『可能』ならば捉える『べき』であり、言語の持つ制限はツールで補える、と主張するのです。
|
|
生産性が向上する要因の最後として、コンパイル・ステップが無いことが挙げられます。動的型定義の言語の多くはインタープリター型であるため、変更の後、すぐに結果を見ることができます。Rubyでは、通常はデバッグ・セッションのコンテキストでインタープリターを開き(これについては前回の記事で説明しました)、そして単純に動作を調べることができるため、通常のデバッガーが無くてもライブラリーやアプリケーション・コードの振る舞いを簡単に探ることができます。
そしてさらに・・・
ただしコンパイルは、静的型定義をサポートすること以上のことをします。静的言語を支持する人達は、パフォーマンスも良いのだと主張します。JavaコードやC++などといった多くの静的言語は『システム言語』と呼ばれますが、これはオペレーティング・システムやデバイス・ドライバーその他、ハイ・パフォーマンスのシステム・コードを書く際に最も一般的に使われるのが、こうした言語であるためです。そのため動的言語の支持者は、アプリケーション用の生産的言語であるためには静的型定義言語は下位レベル過ぎる、と非難しがちです。しかし、それは非常に視野の狭い見方です。OCaml言語は非常に上位レベルの言語ですが、オブジェクト指向プログラミングや、(LispやErlangのような)ファンクショナル・プログラミングが可能であり、また伝統的な構造化プログラミングも行うことができます。その型定義モデルは静的であり、パフォーマンスはC++よりも高い、と言う人が数多くいます(参考文献を見てください)。OCamlは推論型定義の言語であるため、静的型定義のためのオーバーヘッドは、ごく僅かです。その上、極めて安定したパフォーマンスが得られ、コンパイル時の型チェックが可能であり、しかも非常に上位レベルの言語なのです。アヒル型定義を心底から信奉する人であっても、こうした点には一目置くべきでしょう。
Java言語での型定義の制約
Java開発者は、静的型定義を最大限に利用しています。彼らは、コード完了やリファクタリングなどの機能を備えた世界最高の開発ツールを持ち、それらのツールは大きく静的型定義に傾いています。コンパイラーは型に関係するバグを捉えてくれるため、「まずテスト」という開発(test-first development)を最大限に利用し始めた多くのJava開発者は、さらに安定なものを手にすることができます。ジェネリックスなどの新機能によって型定義モデルが強化されるだけではなく、より多くの情報がコンパイラーに提供されます。しかしJava開発者は、動的型定義の利点に目をつぶりがちなのです。
ランタイム・バインディング
動的型定義による柔軟性は、皆さんが想像するよりも重要です。Java開発者は、XML(実行時までバインディングを遅らせます)やストリング(様々な種類の型を表現できます)を多く使うことによって、ある意味で静的型定義を帳消しにしています。コンフィギュレーションは多くの場合ランタイム・バインディング(従って静的型定義)で効果がありますが、Rubyでのコンフィギュレーションは通常はRubyコードの形をとり、Javaプログラミングでは通常XMLの形をとります。例えばSpringフレームワーク(参考文献を見てください)では、ジェネリックなSpring beanをコンフィギュレーションするためにはXMLを使います。そして各変数に対して有効なJavaクラス名を与え、プロパティーを設定する必要があります。例えばパーシスタンス・エンジン(Hibernateなど)は、セッション・ファクトリー(参考文献を見てください)を必要とします。Javaの構文でデータ・アクセス・オブジェクトをコンフィギュレーションしようとすると、極めて楽しいことになります。
Dao myDataAccessObject = Dao.new(sessionFactory); |
問題は、このコード行がコンパイル時にバインドされることです。それでは静的過ぎるのです。セッション・ファクトリーやデータ・アクセス・オブジェクトのインスタンスを、何か別なものと置き換えなければならない場合がよくあります(例えばテスト用の、偽のデータ・アクセス・オブジェクトなど)。そこで、上記の例のようにハードコード化するのではなく、Springフレームワークのようなフレームワークを使って、XMLでアイテムをコンフィギュレーションします。そうすると、次のようになります(Springフレームワークの、petclinicという例より引用しています)。
<bean id="myDao" class="org.springframework.samples.petclinic.hibernate.HibernateClinic">
<property name="sessionFactory" ref="sessionFactory">
</bean>
|
Springフレームワークは、今日のJavaコミュニティーにおいて最も重要かつ影響力のあるフレームワークの1つですが、その理由はバインディングを遅らせることができ、またシステム中の主要要素間の結合を疎にできるためです。さらに、継承時のある狙いを無駄にすることなく分離することができます。Javaプログラミングでは、特にPOPJ(plain old Java object: 昔ながらの単純Javaオブジェクト)を書くことが多い場合には、継承を使う際には十分に注意する必要があります。Java言語では、継承はある狙いしか得られないのです。
Rubyのような動的言語では、ソリューションは驚くほど異なります。まず、パーシスタンスを実装するために『ミックス・イン(mix-in)』をよく使うようになります。すべての関連付けは、一度ミックス・インの中で起こります。ミックス・インは、背後に実装を備えたインターフェースと考えることができます。言い換えると、ミックス・インによって、同じオブジェクトに対して(複数の継承を使うことなく)複数の機能を追加できるのです。実際、Active Recordは、共通のベース・クラス(複数の機能をミックス・インしています)から継承することによって、正にこれを行います。
class Pojo < ActiveRecord::Base |
Rubyでは、継承時の一発勝負を気にする必要はありません。オープンなクラス(オンザフライで機能を追加できます)とオープンなモジュール(さらに他の機能をミックスできます)によって、より多くの機能を自在にオブジェクトに追加できるのです。では密結合の問題はどうなのでしょう。このクラスをJava流に実装したいのであれば、次のようなものになります。
class MyClass
attr_accessor myDao #defines getters and setters for myDao
def initialize(session_factory)
myDao = Dao.new(session_factory)
end
...
|
initialize() メソッドの中のコードは、まるで最初のバージョンのJavaのようです。コンパイル時にデータ・アクセス・オブジェクトをセッション・ファクトリーにバインドしているので、これはタブー(taboo: 禁制)のはずです。しかしこれは動的型定義の言語なので、固く考えることはありません。テスト用として、いつでもクラスの定義をオンザフライで変更できます。そして後で既存のクラスを開けばよいのです。
class MyClass #not redefining the class; just opening the existing class
def myDao #redefine the getter for myDao
#do some work to generate the mock object
return myMockObject
end
end
|
まとめ
皆さんはプログラミング言語のユーザーとして、良きにつけ悪しきにつけ、ある時点でその言語の型定義戦略の奴隷となっています。皆さんはJavaプログラマーとして、その型定義システムを尊重する方法でJavaコードを書く方法を追求すべきです。型定義システムを最大限に生かし、また、独自のメタプログラミングを行うのではなくフレームワークによってメタプログラミングを行うためにコミュニティーに頼ることは、どちらも良い方法です。メタプログラミングをサポートしたJavaフレームワークは膨大な数があります。それらは、パーシスタンスの面から(HibernateとJDO)、トランザクションの面から(SpringやEJB)、モデル/ビュー/コントローラーの面から(WebFlowやRIFE)、そしてプログラミング・モデルの面から(AspectJ)メタプログラミングをサポートしています。
しかし場合によると、自分が選択した言語の型定義システムに反して作業しなければならないことがあります(例えば、記述を追加して読みやすくするためにコードをドキュメント化する場合や、ある型に対するバインディングを遅らせたい場合など)。Java言語は非常に強力なため、そうしたことをするために構築された、次のような多くのプロジェクトを最大限に利用することができます。
-
Springフレームワークを使うと、バインディングを実行時まで遅らせることができ、また動的型定義言語の特徴である多くの機能を利用することができます。Springは、POPJへの機能追加や、ランタイム・コンフィギュレーション、Java言語の型定義による制約回避などに特に適しています。
-
AspectJはJavaプラットフォーム上でのアスペクト指向プログラミング・モデルの実装です(参考文献を見てください)。AspectJでは、追加の構文を導入せずにコンサーンのクロスカット(静的言語であるJavaの特性を克服する手法)を導入することができます。
-
HibernateプロジェクトとJPA(Java Persistence API)を使うと、(この場合も基礎となっている型を変更することなく)POPJにパーシスタンスを追加することができます。
-
XMLを使うと、データとアプリケーション両方のコンフィギュレーションを表現することができます。多くのフレームワークは、Java言語の型定義の制約を、XMLを使って回避しています。
また、さらに別の選択肢もあります。つまり他の言語の型定義戦略を理解できれば、Javaの戦略には向かない問題を認識することができます。そしてJavaの『プラットフォーム』にアクセスする必要はあるもののJava『言語』は必要ない場合には、他の言語による優れたJVM実装(参考文献を見てください)を使うことができるのです。
このシリーズの次回の記事では、Ruby on Railsでのテストについて見て行きます。RailsとJava言語は一部の概念を共有しており、一面ではRailsの方が優れていますが、Javaの方が明らかに勝っている領域もあります。その説明を行う次回まで、境界を越えながら、他の世界との接触を続けてください。
参考文献 学ぶために
製品や技術を入手するために
-
Objective Caml を入手してください。Lispの派生であるOCamlは静的型定義と推論型定義を組み合わせたものであり、生産性を犠牲にせずにハイ・パフォーマンスを実現します。
- 皆さんの次期開発プロジェクトを、IBM trial softwareを使って構築してください。developerWorksから直接ダウンロードすることができます。
著者について  | 
|  | Bruce Tateは父であり、マウンテンバイク乗りであり、カヤック乗りであり、そしてテキサス州オースチンに住んでいます。Joltを受賞した『Better, Faster, Lighter Java』を含め、ベストセラーとなった3冊の著書を執筆しています。最近、『Spring: A Developer's Notebook』を発刊しました。彼はIBMに13年間在籍した後、J2Life, LLC社を設立し、Java技術とRubyに基づく軽量開発戦略とアーキテクチャーを専門としたコンサルティングを行っています。 |
記事の評価
|