[技術講座]
オブジェクト指向言語は間違っていた! --- 新世紀言語 MixJuice ---
産業技術総合研究所 一杉 裕志
■ はじめに
「オブジェクト指向言語は間違っていた!」などとトンデモ系のタイトルにしてみましたが、正確に言うと、「従来の C++, Java などのオブジェクト指向言語における モジュール機構には問題がある。そこで新しいモジュール機構を設計した。」ということです。
MixJuice (以下 MJ と略) は、Java 言語を改良した言語です。最大の特徴は「差分ベースモジュール」と呼ぶモジュール機構です。このモジュール機構は、Java 言語のモジュール機構よりも はるかにシンプルであるにもかかわらず、プログラムの再利用性・拡張性・保守性をより向上させます。
具体的には、差分ベースモジュールは以下のような特長を持ちます。
- 拡張性の高いアプリケーションが簡潔に書ける。
- クラスとモジュールが直交しており、モジュール化の自由度が高い。
- アプリケーションのエンドユーザが、実装の詳細を知らなくてもモジュールを組み合わせて使うことができる。
この記事の目的は、差分ベースモジュールの特徴を知ってもらい、議論・開発に参加する人の輪を広げることです。差分ベースモジュールは、プログラマーとしての自分が、長年追い求めていた理想に近いモジュール機構です。この機構が、21世紀のプログラミング言語の 標準的機能になればと思います。
■ 従来の「クラスベースモジュール」の問題点
現在のオブジェクト指向言語の主流である C++ や Java などの言語では、class という構文が、モジュールとしての機能も持っています。これを「クラスベースモジュール」と呼びます。クラスとは本来オブジェクトの雛型であり、モジュールとは再利用、情報隠蔽、分割コンパイルの単位です。この2つは、異なる概念です。
クラスに再利用・情報隠蔽の機能を持たせると、スタックのような抽象データ型だけを扱う場合ついては、うまく機能します。しかし、現実のより複雑なアプリケーションでは不十分です。このことは多くのプログラミング言語研究者によって指摘されています。(例えば最近では Aspect-oriented programming が有名です。)
私が考えるクラスベースモジュールの明確な限界点は、「コラボレーション」を再利用の単位にできない、という点です。現実のプログラムでは、1つのクラスは複数のロール(役割)を持ち、 複数のロールが協調することで1つの機能(コラボレーション)を実現します。 コラボレーションは、複数のクラスを横断することになります。あとで述べるように、クラスではなくコラボレーションを 再利用・情報隠蔽・分割コンパイルの単位にできれば便利なのですが、クラスベースモジュールでは、複数のクラスを横断するコードを分離することは不可能です。
■ 「差分ベースモジュール」:モジュール=差分
MJ は、Java言語が本来持っているモジュール機構の代わりに、「差分ベースモジュール」というモジュール機構を採用した言語です。このモジュール機構により、下図のように、複数のクラスをまたがったコードも、別々のモジュールに分割して記述することができます。
MJでは、もはやクラスはモジュールとしての機能を一切持ちません。つまり、クラスは再利用・情報隠蔽・分割コンパイルの単位ではありません。
では、MJにおけるモジュールとは何か? MJでは、モジュールとは「差分」です。オリジナルのプログラムと拡張されたプログラムの間の差分を、モジュールとして記述します。つまり、個々のモジュールは、patch fileのようなものです。ただし、patchは差分を文字列レベルで処理するため、複数の差分を同時に追加しようとすると失敗する場合があります。一方、MJのモジュールは、言語レベルで差分を処理するため、差分を追加した結果のプログラムの型的安全性が保証されます。
差分ベースモジュールを用いることにより、プログラマーは、プログラムを機能(コラボレーション)ごとに異なるモジュールに分割して記述することが可能になります。モジュールは分割コンパイルの単位であるため、異なるプログラマーによって別々に開発することも可能です。また、あるアプリケーションの機能を拡張するモジュールを、サードパーティのプログラマが開発することも可能です。
MJのモジュールの特徴の1つに、「エンドユーザによる結合が可能」という点があります。エンドユーザは、様々なプログラマーによって開発されたモジュールのうち、自分が欲しい機能を実現するものだけを選んで組み合わせることで、自分独自のアプリケーションを構築することができます。このとき、モジュールをつなぐための、いわゆる「glue code」を一切書く必要はありません。エンドユーザは、使いたいモジュール名の集合をリンカーに指定するだけで、リンカーは自動的に指定されたモジュールを結合し、1つのアプリケーションにします。モジュールの結合はpatchを当てることに相当しますから、glue codeは必要ないのです。
■ クラス継承とモジュール継承
個々のモジュール同士は継承関係を持ちます。MJには、モジュールの継承機構と従来のクラスの継承機構の両方が独立に存在しています。クラス継承とモジュール継承は、次のように違います。
- クラス継承は既存のクラスに対する変更部分を記述する機構ですが、モジュール継承は既存のプログラム全体に対する変更部分を記述する機構です。
- クラス継承では、すでに存在するクラスとは違う名前を持った、新しいサブクラスを定義することしかできませんが、モジュール継承では、すでに存在するクラスそのものに対し、その名前を変えることなしに、変更を加えることができます。
クラスベースモジュールと差分ベースモジュールにおける、クラスとモジュールの役割分担をまとめると、次のようになります。
■ モジュール定義の構文
簡単なプログラムのソースコードを使って、MJのモジュール定義の構文について説明します。
MJでは、クラス定義の外側をmoduleという構文で囲みます。モジュールの定義は次のように記述します。
module m1 { define class S { define int foo(){ return 1; } } define class A extends S { int foo(){ return original() + 10; } } } module m2 extends m1 { class S { int foo(){ return original() + 2; } } class A { int foo(){ return original() + 20; } } }この例ではモジュールm1とモジュールm2を定義しています。
モジュール定義の先頭の"extends"宣言は、そのモジュールが差分の追加先として想定しているモジュール名を宣言します。指定されたモジュールをsuper-moduleと呼びます。モジュールm2のsuper-moduleはm1です。つまり、モジュールm2は、モジュールm1で定義されたプログラムに対する差分を定義しています。
モジュールm1のように、"extends"宣言を持たないものは、「空のプログラム」に対する差分を定義するモジュールです。
モジュールm2が表すプログラムは、Javaで書けば、次のものに相当します。
class $S1$ { int foo(){ return 1; } } class S extends $S1$ { int foo(){ return super.foo() + 2; } } class $A1$ extends S { int foo(){ return super.foo() + 10; } } class A extends $A1$ { int foo(){ return super.foo() + 20; } }モジュール本体(中かっこの内部)には、オリジナルのプログラムに対する差分を記述します。具体的には、モジュールは、そのsuper-moduleが表すプログラムに対して以下の変更を加えることができます。
- 新たなクラスの追加
- 既存のクラスに対する新たなフィールドの追加
- 既存のクラスに対する新たなメソッドの追加
- 既存のメソッドのoverride
さきほどの例では、モジュールm2は、モジュールm1で定義されたクラスSのメソッドfooと、クラスAのメソッドfooをそれぞれoverrideして、その機能を拡張しています。
"define"というキーワードは、「新たな定義の追加」と「既存の定義の拡張」とを区別するために使われます。つまり、"define"というキーワードの付いたクラス・フィールド・メソッド定義は、 新たなクラス・フィールド・メソッドを追加することを示します。"define"がついていないクラス定義・メソッド定義は、既存のクラスやメソッドの定義を拡張することを示します。
"original()"という式は、overrideされたメソッドを、overrideしているメソッド内から呼び出す場合に使います。これはJava言語におけるsuperへのメソッド呼び出しに相当します。
なお、super-moduleは複数指定することができます。つまりモジュールは多重継承することができます。多重継承によって生じる問題の1つに名前の衝突がありますが、この問題はMJでは「完全限定名」をフィールド名やメソッド名にも持たせることで完全に解決してます。
■ モジュール化の例1:コラボレーションの分離
差分ベースモジュールを使って、複数のクラスをまたがるコラボレーションを、別のモジュールに分離することが可能です。次のようなJavaで書かれたプログラムを考えます。
class A { // class A uses class B void m1(B b){ ... b.m3(); ...} void m2(){...} } class B { // class B uses class A void m3(){...} void m4(A a){ ... a.m2(); ...} }このプログラムは、クラスAとクラスBがそれぞれ相互に依存しているため、モジュラリティの悪い構造をしています。しかし2つのクラスは、実質的には、独立した2つのコラボレーションを含んでいます。このプログラムは、MJでは次のようにモジュール分割することができます。
module A_B { define class A {} define class B {} } module collaboration_m1_m3 extends A_B { class A { define void m1(B b){ ... b.m3(); ...} } class B { define void m3(){...} } } module collaboration_m2_m4 extends A_B { class A { define void m2(){...} } class B { define void m4(A a){ ... a.m2(); ...} } }このようにコラボレーションを別のモジュールに分離することで、以下のメリットが生じます。
- それぞれのモジュールが依存するソースコードのサイズが減ります。これは一般には、保守性が向上することを意味します。
- collaboration_m1_m3とcollaboration_m2_m4は相互に全く依存していないため、他方が存在していなくても、一方をコンパイル・実行することが可能です。このため、別々のプログラマーによる開発・テストが可能になります。
- それぞれのコラボレーションとは違うバージョンのモジュールを実装することにより、異なるコンフィギュレーションのアプリケーションを構築できるようになります。例えば、collaboration_m1_m3の代わりに、my_collaborationというモジュールを実装して用いることが可能になります。この時、他のモジュールの再コンパイルは必要ありません。
■ モジュール化の例2:ネストした if 文
次のようにネストしたif文を、複数のモジュールに分割して記述することも可能です。
class F { void branch(String s){ if (s.equals("a")){ ... } else if (s.equals("b")){ ... } else { throw new Error(); } } }上のJavaプログラムを条件節ごとにモジュール分割したものが下のMJプログラムです。
module framework { define class F { define void branch(String s){ throw new Error(); } } } module case_a extends framework { class F { void branch(String s){ if (s.equals("a")){ ... } else { original(s); } } } } module case_b extends framework { class F { void branch(String s){ if (s.equals("b")){ ... } else { original(s); } } } }このように記述することにより、ソースコードを編集することなしに、新たな条件分岐節を追加することができるようになります。一般にネストしたif文はオブジェクト指向的ではなく、拡張性が悪いと言われていますが、MJではもはやその心配はありません。XMLを処理するプログラムなど、ネストしたif文を書かざるを得ない場合が時々ありますが、このモジュール化手法により拡張性の高い構造にすることが可能です。
■ モジュール化の例3:テーブルの初期化コード
プログラムを初期化する部分のコードは、従来のオブジェクト指向言語では1箇所に集中しがちです。しかし、MJではモジュール分割することができます。例えば次のJavaプログラムを見て下さい。
import java.util.Hashtable; class DataManager { Hashtable table = new Hashtable(); void initTable(){ table.put("A", new A()); table.put("B", new B()); ... } } class A {...} class B {...} ...従来のオブジェクト指向言語では、テーブルの初期化コードは普通、このようなスタイルになります。このスタイルでは、テーブルに新たな要素を追加するためには、初期化メソッドの編集が必要になってしまいます。一方MJでは、同じプログラムを下のようにモジュール分割することができます。
module dataManager imports java.util.Hashtable { define class DataManager { define Hashtable table = new Hashtable(); define void initTable(){} } } module classA extends dataManager { class DataManager { void initTable(){ original(); table.put("A", new A()); } } define class A {...} } module classB extends dataManager { class DataManager { void initTable(){ original(); table.put("B", new B()); } } define class B {...} } ...このようなスタイルで書くことで、ソースコードを修正しなくても、新たなテーブルの要素を追加できるようになります。
■ もっと詳しく知りたい方のために
差分ベースモジュールの特徴の全ては、とてもここでは語り尽くせません。より詳しくは、下記のテクニカルレポートを御覧ください。
産総研テクニカルリポート AIST01-J00002-1
https://staff.aist.go.jp/y-ichisugi/doc/AIST01-J00002-1.pdf差分ベースモジュールの下記の特徴については本記事では説明しませんでしたが、このテクニカルレポートで説明しています。
- 情報隠蔽
- 名前衝突回避
- 実装欠損と補完モジュール
■ 開発の現状
言語の基本設計は完了し、最初のバージョンがソースコードともに公開されています。
開発に協力して頂けるパートナーを募集中です。いまのところコンパイラや実行環境の完成度は、あまり高くありません。ドキュメントやサンプルプログラムも不足しています。興味をもたれた方は御連絡下さい。
© 2001 OGIS-RI Co., Ltd. |
|