このドキュメントでは、 MJ の実行環境と言語仕様について、 ひととおり簡単に説明します。 予備知識は、 Java 言語の知識以外は必要ありません。
このチュートリアルに出てくる全てのプログラムは、 すでにコンパイルして配布パッケージの mj.jar の中に入っています。 「インストールと実行方法」に従って mj をインストールすれば、実際に実行してみることができます。 また、ソースコードは、 配布パッケージの lib/Tutorial.java の中にあります。
このチュートリアルでは、 Collection plug-in、 assert2 plug-in、 については 触れません。それぞれのドキュメントを参照してください。
なお、 MJ におけるプログラミングスタイルや設計方法論については、 このドキュメントは説明の対象としません。
MJ の言語仕様の詳細については、「MJ言語仕様メモ」を 参照してください。 そこでは、BNF による文法 の記述もあります。
まずは空の作業ディレクトリを作って、そこで実行をはじめるとよいと思います。 作業ディレクトリには、 eppout というディレクトリを作っておく必要があります。
% mkdir eppout
ディレクトリ eppout の下には、プリプロセッサが出力する java ファイルと、コンパイル結果のクラスファイルが置かれます。
ソースコードのコンパイルの説明をする前に、 まず実行の仕方から説明します。
mj.jar の中には t.m1 というモジュールが含まれています。 このプログラムのソースは、 Java で言えば、次のものに相当します。 (MJ で書かれた本当のソースは、あとでお見せします。) これは、クラス C とそのサブクラス SubC を定義する、簡単なプログラムです。
class C { void m() { System.out.println("m1:C#m()"); } } class SubC extends C { void m() { super.m(); System.out.println("+ m1:SubC#m()"); } } class SS { public static void main(String[] args){ System.out.println("----- invoke C#m();"); C c = new C(); c.m(); System.out.println("----- invoke SubC#m();"); SubC subc = new SubC(); subc.m(); } }
モジュールt.m1は実行可能です。 これを実行してみましょう。 MJ で書かれたプログラムを実行するには、mj コマンドを使います。 mj コマンドは、モジュール名を1つ引数に取ります。
% mj t.m1 ----- invoke C#m(); m1:C#m() ----- invoke SubC#m(); m1:C#m() + m1:SubC#m()
このように実行されます。
同様に、 t.m2 というモジュールも mj.jar の中に入っています。 t.m2 は、 t.m1 に差分を追加したプログラムです。 t.m2 は、 Java で書けば次のようになります。 つまり、 t.m1 のソースに2行追加したプログラムです。
class C { void m() { System.out.println("m1:C#m()"); System.out.println("+ m2:C#m()"); // 追加 } } class SubC extends C { void m() { super.m(); System.out.println("+ m1:SubC#m()"); System.out.println("+ m2:SubC#m()"); // 追加 } } class SS { public static void main(String[] args){ System.out.println("----- invoke C#m();"); C c = new C(); c.m(); System.out.println("----- invoke SubC#m();"); SubC subc = new SubC(); subc.m(); } }
t.m2 を実行してみましょう。
% mj t.m2 ----- invoke C#m(); m1:C#m() + m2:C#m() ----- invoke SubC#m(); m1:C#m() + m2:C#m() + m1:SubC#m() + m2:SubC#m()
このように実行されます。
t.m2 のように、オリジナルのシステムに少し差分を追加するプログラムを、 別のモジュールとして分離して書けることが MJ の特徴です。 従来のオブジェクト指向言語のクラス継承の機構と似ていますが、 superclass C のメソッドそのものを拡張できる点が違います。 クラス継承では、 superclass のメソッドの機能を拡張するには、 別の subclass を作る必要があります。 subclass を定義しても、 superclass のメソッドは拡張される前のままです。 ところが MJ では、 superclass のメソッドを、別の subclass を定義することなしに 拡張することができます。
具体的に、 MJ でモジュールをどのように記述するかを以下に説明します。
まず t.m1 の MJ で書かれたソースを見てみましょう。
module t.m1 { define class C { define C(){} define void m(){ System.out.println("m1:C#m()"); } } define class SubC extends C { define SubC(){} void m(){ original(); System.out.println("+ m1:SubC#m()"); } } class SS { void main(String[] args){ System.out.println("----- invoke C#m();"); C c = new C(); c.m(); System.out.println("----- invoke SubC#m();"); SubC subc = new SubC(); subc.m(); } } }
Java 言語による記述とまず違うのが、 "module" という構文です。 これは Java 言語における package にほぼ相当し、 この構文の中にクラス定義を書きます。
モジュール内部の記述では、 "define" というキーワードの存在が、 Java と比べたときの大きな違いです。 クラス名やメソッドセレクタを最初に定義するときには、 この "define" を指定する必要があります。 superclass のメソッドを継承して override する場合は、 "define" はつけません。 MJ でのプログラミングでは、 定義と拡張の区別をプログラマーが意識することが重要なため、 このキーワードが導入されました。 Java や C++ では、「 superclass のメソッドを override したつもりが、 メソッド名のスペルミスのせいで、別のメソッドを定義していた」という ミスが時々起こります。このミスは、コンパイルエラーにはならないため、 見つけるのに手間取る場合があります。 "define" キーワードの指定は、この種のエラーをコンパイル時に検出するためにも 役立ちます。 なお、 "define" キーワードは、 field の定義には必要ありません。 field には「拡張」という概念がなく、定義しかありえないからです。 field 定義に "define" キーワード指定しても無視されます。
main の書き方も Java とは違います。 現在の MJ は static method をサポートしていません。 代わりに、 SS という名前のクラスが特別な役割を果たします。 SS は、 Startup-Singleton の略です。 MJ のアプリケーションが起動すると、クラス SS のインスタンスが1つ生成され、 そのメソッド main(String[]) が呼び出されます。 class SS には "define" がついていません。 この理由は「モジュール mj.lang.ss と クラス SS」 の章で説明します。
MJ では、 super class のメソッドを呼び出すには、 super.m() ではなく original() と書きます。 このプログラム例では、 class SubC のメソッド m で、 original 呼び出しを行っています。これは、 class C のメソッド m を 呼び出します。
public/protected/private というキーワードは、 MJ では使いません。 指定しても無視されます。
次に t.m2 の MJ で書かれたソースを見てみましょう。
module t.m2 extends t.m1 { class C { void m(){ original(); System.out.println("+ m2:C#m()"); } } class SubC { void m(){ original(); System.out.println("+ m2:SubC#m()"); } } }
t.m2 は、 t.m1 に対する差分だけを記述するモジュールです。 先頭の "module t.m2 extends t.m1" という部分がそれを表しています。 t.m1 は t.m2 の super-module 、 t.m2 は t.m1 の sub-module 、と呼びます。
モジュール本体では、オリジナルのプログラム(すなわち super-module) に対する差分を記述します。 クラス C およびクラス SubC の宣言に "define" がありませんが、 これは、 super-module で定義されたクラスに対する差分を定義することを 意味します。 クラス C および クラス SubC のメソッド m() にも "define" がありませんが、 これも、オリジナルのメソッドに対する差分を定義していることを意味します。
それぞれのメソッド m() の本体には、 original(); という構文があります。 これは、「 super-module におけるオリジナルのメソッド」への呼び出しを 意味します。
メソッドの定義や差分を、「メソッド断片」とも呼ぶことがあります。 例えば t.m2 における SubC のメソッド断片 m() の定義を、 SubC#m()@t.m2 と 表記することにします。
t.m1 と t.m2 に含まれる4つのメソッド断片の関係を図で表すと、 次のようになります。
module t.m2 module t.m1 +-------------+ +-------------+ | | | | 3:|C#m()@t.m2 | -> 4:|C#m()@t.m1 | | | difference | | | | | ^ inherit | | | | | 1:|SubC#m()@t.m2| -> 2:|SubC#m()@t.m1| | | difference | | +-------------+ +-------------+
図中の1:〜4:の数字は、 original 呼び出しで呼び出されるメソッド断片の 順序を表します。 まずクラス SubC のインスタンスに対してメソッド m() が呼び出されると、 1番目のメソッド断片 SubC#m()@t.m2 がまず実行されます。 このメソッド断片 SubC#m()@t.m2 の実行中に、 もし original 呼び出しが実行されると、 2番目のメソッド断片 SubC#m()@t.m1 が呼び出されます。 以後同様に、 C#m()@t.m2 、 C#m()@t.m1 が呼び出されます。 MJ における差分の追加とは、 オリジナルのプログラムにおける superclass 、 subclass の それぞれのメソッド定義を override することであると考えれば、 この動作は自然に理解できると思います。
t.m2 を実行する際に、 mj コマンドの引数にはモジュール名 t.m2 のみを指定します。 t.m1 の名前は指定する必要はありません。 mj コマンドは、引数で指定されたモジュールを まず load し、その実行に必要なモジュール、すなわち super-module も load します。同様に、すべての先祖モジュールも load します。 この場合は、 t.m2 を指定すれば、 mj コマンドが自動的にその super-module である t.m1 も load します。
この機能は、 Emacs Lisp, Common Lisp などのシステムにおける require 宣言の機能に似ています。 これらのシステムでは、 あるパッケージが必要とする他のパッケージを "require" という構文で 宣言します。あるパッケージをシステムに load すると そのパッケージが require するパッケージも自動的に load されます。 MJ における extends 宣言は、この require 宣言の機能も兼ねているのです。
モジュールを定義するとき、 super-module を複数指定することができます。
まず、 t.m2 とほぼ同様のプログラム t.m3 があるとします。
module t.m3 extends t.m1 { class C { void m(){ original(); System.out.println("+ m3:C#m()"); } } class SubC { void m(){ original(); System.out.println("+ m3:SubC#m()"); } } }
t.m3 を実行すると次のようになります。
% mj t.m3 ----- invoke C#m(); m1:C#m() + m3:C#m() ----- invoke SubC#m(); m1:C#m() + m3:C#m() + m1:SubC#m() + m3:SubC#m()
そして、 t.m2 と t.m3 の両方を継承するモジュール t.m4 を、次のように定義できます。
module t.m4 extends t.m2, t.m3 { class C { void m(){ original(); System.out.println("+ m4:C#m()"); } } class SubC { void m(){ original(); System.out.println("+ m4:SubC#m()"); } } }
ここで、 t.m2 と t.m3 は共に t.m1 を super-module に持つので、 いわゆるダイアモンド継承の形になります。 MJ では、 CLOS などの言語と同様に、 topological sort によって モジュールの linearize (1列に並べること)をします。
例えば、 t.m4 の実行は、次のように行われます。 まず、 t.m4 とその先祖モジュールの集合を求めます。 それは {t.m1, t.m2, t.m3, t.m4} です。 そして、この集合を super-module/sub-module の上下関係を保存するように topological sort することによって linearize します。 linearize した結果を、 linearized list と呼びます。 この場合、 linearized list は (t.m1 t.m2 t.m3 t.m4) になります。 そして、 linearized list の最初の方から順に差分を追加していき、 できあがったプログラムを実行します。 つまり、プログラム a に対して差分 b を追加した結果を a <- b と表記するなら、 (((t.m1 <- t.m2) <- t.m3) <- t.m4) を実行します。 したがって、 t.m4 の実行結果は、次のようになります。
% mj t.m4 ----- invoke C#m(); m1:C#m() + m2:C#m() + m3:C#m() + m4:C#m() ----- invoke SubC#m(); m1:C#m() + m2:C#m() + m3:C#m() + m4:C#m() + m1:SubC#m() + m2:SubC#m() + m3:SubC#m() + m4:SubC#m()
ここで、 linearize するときに、 上下関係が定義されていない t.m2 と t.m3 の 順序をどうするか、という問題があります。 MJ は、 t.m4 の super-module 宣言における順序は、無視します。 つまり、次のいずれの書き方をしても、動作に影響は与えません。
module t.m4 extends t.m2, t.m3 {...} module t.m4 extends t.m3, t.m2 {...}
MJ の言語処理系は、言語仕様によって決められたアルゴリズムにより、 t.m2 と t.m3 の間の順序を決めます。 (現在のところ、モジュールの名前の情報を使って、 "monotonic" な linearize を行うアルゴリズムを採用しています。) プログラマーが t.m2 と t.m3 の間の順序を制御する方法はありません。 また、プログラマーは、 t.m2 と t.m3 の間の順序を仮定してプログラムを書いてはいけません。
MJ とは違い、 CLOS などの言語では、 superclass の並べ方が優先度に影響を与えます。 これを local precedence order と呼びます。 MJ でもこの機能を入れることは可能ですが、 現在は入れていません(FAQ 参照)。
C++ などの言語における多重継承の最大の問題点は、 名前の衝突です。 MJ ではこの問題は完全に解決されています。 これについては 「完全限定名」の章で説明します。
アプリケーションのエンドユーザは、 独立に開発された複数の差分を、 コードを1行も書かずに組み合わせて使うことができます。 (MJ のモジュールのこの性質を、 plug-and-play module と呼んでいます。)
例えば t.m2 と t.m3 は t.m1 に対する全く独立した差分ですが、 エンドユーザが、 2つの差分を両方を同時に t.m1 に追加することができます。 それには、 mj コマンドの -s オプションを使い、複数のモジュールを選択します。
% mj -s t.m3 t.m2 ----- invoke C#m(); m1:C#m() + m2:C#m() + m3:C#m() ----- invoke SubC#m(); m1:C#m() + m2:C#m() + m3:C#m() + m1:SubC#m() + m2:SubC#m() + m3:SubC#m()
mj コマンドの引数に指定されたモジュールの集合 {t.m2, t.m3} の要素を、 selected module と呼びます。
-s オプションによって、 selected module が複数指定された場合の mj コマンドの動作は、次のようになります。 上の例のように selected module の集合が { t.m2, t.m3 } だったとします。 mj コマンドは、全ての selected module を extends する仮想的なモジュールを、 "_bottom" という名前で作ります。このモジュールを bottom module と呼びます。 つまり、この場合、次のようなモジュールを作ります。
module _bottom extends t.m2, t.m3 {}
そして、あたかも "mj _bottom" というコマンドが実行されたかのように 処理します。 つまり、 _bottom とその先祖モジュールの集合を求め、 それを super-module/sub-module の関係を保存するように linearize して linearized list を作り、 linearized list の上から順に差分を追加して行き、 できあがったアプリケーションを実行します。 今の例の場合、 linearized list は (t.m1 t.m2 t.m3 _bottom) となります。
extends 宣言でのモジュールの並べ方が実行に影響を与えないのと同様に、 -s オプションによる selected module の並べ方も、実行に影響を与えません。 例えば、次のどの方法で実行しても 結果は変わりません。
% mj -s t.m2 t.m3 % mj -s t.m3 t.m2
1つの差分を同時に何度も追加することは(現在の MJ では)できません。 同じモジュール名を -s オプションで複数回指定しても、 1つ指定した場合と効果は変わりません。 例えば、次の例は、いずれも同じ動作をします。
% mj -s t.m2 t.m3 % mj -s t.m2 -s t.m2 t.m3
さて、ここでようやくコンパイルの仕方の説明に入ります。 これは、モジュール2つから成る Hello World プログラムのソースコードです。
module t.hello { class SS { void main(String[] args){ original(args); System.out.println("Hello."); } } } module t.world extends t.hello { class SS { void main(String[] args){ original(args); System.out.println("World."); } } }
2つのモジュールは、2つのファイルに分けてもいいし、 1つのファイルに書いてもかまいません。 ソースファイルは、 ".java" で終わらなければなりません。 その点以外は、ファイル名とファイルの置き場所には制限はありません。 (1つのモジュールを複数のファイルに分けて書くことはできません。)
1つのソースファイル中に複数のモジュールを書く場合、 ソースファイル中のモジュールの順序はプログラムの意味に影響を与えません。
ソースファイルをコンパイルするには、 mjc コマンド を用います。 仮にそれぞれのモジュールを Hello.java, World.java に書いたとします。 この2つのモジュールをコンパイルするには、以下の方法があります。
% mjc Hello.java World.java または % mjc World.java Hello.java または % mjc Hello.java; mjc World.java
次のように World.java だけを指定した場合は、 モジュール world が依存しているファイル Hello.java は コンパイルされないので、注意してください。
% mjc World.java
mjc の処理は、 EPP による preprocess と、 javac による compile の 2つのフェーズから成ります。 コンパイルが正常に終了すると、メッセージの出力は次のようになります。
% mjc Hello.java World.java Preprocessing phase. Compiling phase. Done.
ほとんどのコンパイルエラーは、 Preprocessing phase で出るはずです。 ある種のエラーは、 Compiling phase で出ます。
コンパイルずみのモジュールのクラスファイルの置き場所が CLASSPATH の中に 入っているなら、 そのモジュールをソースコードの中から参照することができます。
コンパイル後のクラスファイルは eppout の下にできるので注意してください。 /a/b/c というディレクトリで mjc コマンドを実行したなら、 その時できたクラスファイルを参照するには、 /a/b/c/eppout を CLASSPATH に含める必要があります。 (mj コマンド、 mjc コマンドは、 eppout を自動的に CLASSPATH に含めるため、 単一のディレクトリで作業している限りは、このことを意識する必要はありません。)
次のようにして eppout の下のクラスファイルを jar にまとめることもできます。 ( mjclear コマンド は、 eppout の下のクラスファイルを削除するコマンドです。)
% mjclear % mjc Hello.java Preprocessing phase. Compiling phase. Done. % cd eppout % jar cvf0 ../hello.jar .
この jar ファイルを CLASSPATH で指定することで、 ライブラリとして、 mjc コマンドなどから参照することができます。
% setenv CLASSPATH hello.jar:$CLASSPATH % mjclear % mjc World.java Preprocessing phase. Compiling phase. Done. % mj t.world Hello. World.
細かいモジュールがたくさんあると、 それらを1つにまとめた方が便利な場合があります。 それを行うのが mjb コマンドです。 mjb コマンドは、次のような形式で実行します。
% mjb bottom m1 m2 m3 ...
mjb コマンドは、第二引数以降で指定されたすべてのモジュールを extends する module を、第一引数で指定された名前で生成します。
例えば、
% mj -s t.m2 t.m3
は、次の実行と同じ結果になります。
% mjb m2m3 t.m2 t.m3 % mj m2m3
この "mjb m2m3 t.m2 t.m3" の実行は、 次のプログラムをコンパイルしたことと等価です。
module m2m3 extends t.m2, t.m3 {}
ソースファイルの中に細かいモジュールがたくさんあると、 それらをすべて extends する bottom module を定義したくなります。 そのようなときに用いるのが mjball コマンドです。 mjball コマンドは、「直前の mjc コマンドがコンパイルしたすべてのモジュール」を extends するモジュールを生成します。
例えば、 A.java, B.java, C.java に a, b, c という3つのモジュールが 定義されているとします。 次の3通りの方法は、いずれもこの3つのモジュールを組み合わせて実行します。
% mjc A.java B.java C.java % mj -s a -s b c
% mjc A.java B.java C.java % mjb all a b c % mj all
% mjc A.java B.java C.java % mjball all % mj all
mjdump コマンドは、 ClassLoader が使えない環境で MJ のアプリケーションを実行したい場合などに 使うコマンドです。
mj コマンドの動作は、次の2つの部分に分かれます。
したがって、 applet など ClassLoader が使えない状況では、 mj コマンドによる実行はできません。
mjdump コマンドは、バイトコード編集されたクラスファイルを JavaVM に load せずに、ファイルシステムに出力するコマンドです。
mjdump の実行例:
% mjdump -s t.m2 t.m3 Dump class : eppout/mjdump/mjc/Start.class Dump class : eppout/mjdump/mjc/SS.class Dump class : eppout/mjdump/t/m1/_Delta_java_lang_Object.class Dump class : eppout/mjdump/t/m1/_Delta_t_m1_C.class Dump class : eppout/mjdump/t/m2/_Delta_t_m1_C.class Dump class : eppout/mjdump/t/m3/_Delta_t_m1_C.class Dump class : eppout/mjdump/t/m1/C.class Dump class : eppout/mjdump/t/m1/_Delta_t_m1_SubC.class Dump class : eppout/mjdump/t/m2/_Delta_t_m1_SubC.class Dump class : eppout/mjdump/t/m3/_Delta_t_m1_SubC.class Dump class : eppout/mjdump/t/m1/SubC.class Dump class : eppout/mjdump/mj/lang/ss/_Delta_mjc_SS.class Dump class : eppout/mjdump/mj/lang/ss/_Delta_mj_lang_ss_SS.class Dump class : eppout/mjdump/t/m1/_Delta_mj_lang_ss_SS.class Dump class : eppout/mjdump/mj/lang/ss/SS.class
出力結果は eppout/mjdump の下にあります。 この下に出力されたクラスファイルは、普通の Java のクラスファイルと 全く同じように実行することができます。 main メソッドは、 mjc.Start というクラスにあります。 実行は次のように行います。
% cd eppout/mjdump % java mjc.Start ----- invoke C#m(); m1:C#m() + m2:C#m() + m3:C#m() ----- invoke SubC#m(); m1:C#m() + m2:C#m() + m3:C#m() + m1:SubC#m() + m2:SubC#m() + m3:SubC#m()
mjdump で出力したクラスファイルは ClassLoader を使わない普通の Java プログラムなので、 applet や MIDlet として使うこともできます。
mj コマンドには起動が遅い(1〜2秒ほど)という問題がありますが、 mjdump コマンドを使って出力したクラスファイルは、 普通の Java アプリケーションと同様、瞬時に実行できます。
class SS は、実は、 mj.lang.ss というモジュールで、 ほぼ次のように定義されています。
module mj.lang.ss { define class SS { static SS instance; define SS(){} define void main(String[] args){} } }
t.m1 は、 class SS を参照しています。 つまり、 t.m1 は mj.lang.ss に対する差分の追加であり、 本来なら次のように宣言すべきです。
module t.m1 extends mj.lang.ss {...}
しかし、 mj.lang.ss はほとんどすべてのモジュールが extends すべき モジュールなので、プログラマーの便宜を図るため、 コンパイラが自動的に extends mj.lang.ss を挿入して コンパイルするしかけになっています。
現在のところ、自動的に extends されるモジュールは、 mj.lang.ss だけです。
現在のMJ は static method の定義をサポートしていないため、 代わりに singleton object のメソッドを使う必要があります。
mj コマンドは、アプリケーションの起動時にまずクラス SS のインスタンスを生成し、 クラス SS の static field である SS.instance に代入します。 その後、クラス SS の void main(String[]) を実行します。
クラス SS のメソッドは、 SS.instance に入れられているsingleton object を通して 呼び出すことができます。
例:
module t.ss.method { class SS { define int foo(){ return 123; } void main(String[] args){ original(args); A a = new A(); System.out.println(a.bar()); // 1230 } } define class A { define A(){} define int bar(){ return SS.instance.foo() * 10; } } }
MJ では Java 言語と同様にコンストラクタを定義できます。 ただし、Java 言語と比べて以下の違いがあります。
これは、コンストラクタを持つクラス Point の例です。
module t.point.m { define class Point { int x; int y; define Point(int x, int y){ this.x = x; this.y = y; } define void move(int dx, int dy){ x += dx; y += dy; } define int getX(){ return x; } define int getY(){ return y; } } class SS { void main(String[] args){ original(args); Point p = new Point(10, 10); p.move(1, 2); System.out.println(p.getX()); // 11 System.out.println(p.getY()); // 12 } } }
現在のバージョンのMJ コンパイラによる、コンストラクタの 実現方法について解説します。
new C(...) というコンストラクタ呼び出しは、 new C()._call_init_C(...) にマクロ展開されます。
define C(...){...} というコンストラクタ定義は、 次のように2つのメソッド定義にマクロ展開されます。
define C _call_init_C(...){ _init_C(...); return this; } define void _init_C(...){...}
define abstract C(...); という "abstract constructor" は、 次のように2つのメソッドにマクロ展開されます。
define C _call_init_C(...){ _init_C(...); return this; } define abstract void _init_C(...);
C(...){...} という「コンストラクタの拡張」は、次のようにマクロ展開されます。
void _init_C(...){...}
コンストラクタの先頭には、 super(...), this(...) が書けます。 super(...) があれば、それは _init_S(...) にマクロ展開されます。 this(...) があれば、それは _init_C(...) にマクロ展開されます。
本来は _call_init_C や _init_C というメソッドはユーザからは見えないように 実装すべきですが、現在の実装では単なるマクロなので、 ユーザからも見えてしまいます。 コンパイルエラーのメッセージやスタックトレースにもこれらの名前が出てくるので、 注意してください。
クラス名を定義するときに abstract という modifier を付けると、 そのクラスは abstract class になります。 abstract でないクラスは、リンク時に abstract method を持っていると エラーになります。(Java とは違い、コンパイル時にはエラーになりません。) abstract class は、 abstract method が残っていても リンク時エラーにはなりません。
これは abstract な class C と abstract でない class SubC を定義する例です。
module t.abst.m1 { define abstract class C { define C(){} define abstract int foo(); } define class SubC extends C { define SubC(){} define abstract int bar(); } class SS { void main(String[] args){ original(args); SubC c = new SubC(); System.out.println(c.foo()); System.out.println(c.bar()); } } } module t.abst.m2 extends t.abst.m1 { class SubC { int foo(){ return 111; } int bar(){ return 222; } } }
t.abst.m1 と t.abst.m2 を実行すると、次のような結果になります。
% mj t.abst.m1 MJLinker: non-abstract class t.abst.m1.SubC has an abstract method : abstract int t.abst.m1::bar()
% mj t.abst.m2 111 222
interface も、 Java 言語と同様に使えます。 メソッドを定義するときは、やはり "define" を付ける必要があります。 interface のメソッドを "implements" するクラス側では、 "define" を付ける必要はありません。例:
module t.color { define interface Color { int RED = 0xff0000; int GREEN = 0x00ff00; int BLUE = 0x0000ff; define int getRGBCode(); } define class ColorImplementation implements Color { define ColorImplementation(){} int code = 0x000001; int getRGBCode(){ return code; } } class SS { void main(String[] args){ original(args); Color c = new ColorImplementation(); System.out.println(c.getRGBCode()); // 1 System.out.println(Color.BLUE); // 255 } } }
interface に対する差分の追加は、現在は完全には実装されていないので、 注意してください。 field (定数)の追加のみが正しく動作します。
abstract method を使って、1つのクラスを、 外部インターフェースのみを定義するモジュール (仕様モジュールと呼びます)と、 実装のみを定義するモジュール(実装モジュールと呼びます)に 分離することができます。 例えば、前節のモジュール t.point.m は、次のように3つの モジュールに分けることができます。
module t.point { define class Point { define abstract Point(int x, int y); define abstract void move(int dx, int dy); define abstract int getX(); define abstract int getY(); } } module t.point.implementation extends t.point { class Point { int x; int y; Point(int x, int y){ this.x = x; this.y = y; } void move(int dx, int dy){ x += dx; y += dy; } int getX(){ return x; } int getY(){ return y; } } } module t.point.test extends t.point { class SS { void main(String[] args){ original(args); Point p = new Point(10, 10); p.move(1, 2); System.out.println(p.getX()); // 11 System.out.println(p.getY()); // 12 } } }
クラス Point の外部インターフェースを定義するモジュールが、 t.point 、 内部実装を定義するモジュールが t.point.implementation です。 モジュール t.point.testは t.point にのみ依存し、 t.point.implementation には依存していない点に注意してください。
このプログラムを実行するには注意が必要です。 t.point.test だけを selected module として指定すると次のように エラーになります。
% mj t.point.test MJLinker: non-abstract class t.point.Point has an abstract method : abstract int t.point::getY()
次のように実装モジュールも selected module に加えるか、 mjball コマンドを使ってすべてのモジュールをひとまとめにしておく必要があります。
% mj -s t.point.implementation t.point.test 11 12
差分ベースモジュールを使って、複数のクラスをまたがるコラボレーションを、 別のモジュールに分離することが可能です。 次のような 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(); ...} } }
このようにコラボレーションを別のモジュールに分離することで、 以下のメリットが生じます。
モジュールの継承は、差分の追加の他に、 名前空間の継承も意味します。
コンパイラは、あるモジュールをコンパイルする際、 そのモジュールが extends していないモジュールは存在しないものとして 処理します。 例えば、次のプログラムは、コンパイルエラーになります。 m2 が extends m1 という宣言をしていないので、 m2 からは m1 の中のクラス A が 見えないからです。
module m1 { define class A {} } module m2 { class SS { void main(String[] args){ original(args); A a = new A(); // error } } }
Java 言語では、 package と nested class を用いて名前空間のネストを表現できます。 MJ では、名前空間のネストは、モジュールの継承によって表現します。 例えば次のような Java のプログラムを考えます。
public class A { protected static int x = 123; public static class B { public int getX(){ return x; } } public static void main(String[] args){ System.out.println(new B().getX()); // 123 } }
これと同様のプログラムを MJ では次のように表現します。
module t.nest.interface_A_B { define class A { } define class B { define abstract B(); define abstract int getX(); } } module t.nest.implementation_A_B extends t.nest.interface_A_B { class A { static int x = 123; } class B { B(){} int getX(){ return A.x; } } } module t.nest.test extends t.nest.interface_A_B { class SS { void main(String[] args){ original(args); System.out.println(new B().getX()); // 123 } } }
このプログラムを実行すると、次のようになります。
% mj -s t.nest.implementation_A_B t.nest.test 123
モジュール interface_A_B はクラス A, B の public な名前を定義し、 モジュール implementation_A_B はクラス A, B の実装と protected な名前を 定義しています。
この例では、クラス B の中からクラス A の static field に対し、 A.x と書いて直接アクセスしています。 このように、 MJ では、 field に限らず、すべての名前は、 同一モジュールあるいは子孫モジュールから 直接アクセスすることができます。 このシンプルなルールだけで、従来のオブジェクト指向言語の public/protected, nested class の機能をすべて表現します (FAQ 参照)。
Java では protected member は subclass からアクセスできますが、 MJ では、名前の参照可能性に関して subclass は特別扱いされません。 したがって、上の例の Java での表現と MJ での表現は、 中身が完全に同一というわけではありません。 MJ のこの仕様が何か不都合を引き起こし得るのかどうかは、 現時点ではわかりません。 今のところは、 MJ では名前の参照可能性に関して subclass の特別扱いは 不要だと考えています。
nested class では、入れ子状の名前空間しか表現できませんが、 MJ ではモジュールの多重継承が可能なため、 重なりがあるような、より一般的な名前空間を表現できます。
MJ からは、 CLASSPATH に入っている Java 言語のクラスライブラリが利用できます。
MJ のクラスは、 Java のクラスを継承したり Java のインターフェースを implements することができます。 このとき、現在の実装ではいくつか制約があります。(詳しくは 言語仕様メモ参照。)
継承に関する制約以外は、制約は(たぶん)全くありません。 すべてのクラスライブラリは、 Java 言語から利用するのと 全く同じように MJ から利用できます。
MJ で定義したクラスは、そのモジュールを extends しないと参照できませんが、 Java のクラスは、完全限定名を指定すれば参照できます。
Java の import 宣言の代わりになるものとして、 MJ には imports 宣言(末尾の s に注意)があります。 module 定義の先頭で imports 宣言しておくことで、 Java 言語と同様、クラスを単純名で参照できるようになります。
imports 宣言の効果は、 sub-module に継承されます。 (経験上この仕様の方が便利です。) 名前空間を汚染しないように、 imports 宣言はできるだけ モジュール継承グラフの下側( sub-module 側)で行った方がよいでしょう。
これは、 imports 宣言を使ったプログラム例です。 imports 宣言の最後に ";" は書かない点に注意してください。
module t.javalib.m1 imports java.io.* imports java.util.Vector { } module t.javalib.m2 extends t.javalib.m1 { class SS { void main(String[] args){ original(args); File f = new File("f"); Vector v = new Vector(); } } }
MJ では、 Java と同様の // 、 /*...*/ というコメントが使えます。 それに加え、 #+identifier <構文> という形式のコメントアウトの 方法があります。 例えば、 #+comment と書くと、その直後の構文1つがコメントアウトされます。 ただしその構文にシンタックスエラーがあってはいけません。
#+identifier の後ろに書ける構文は、 module 定義、クラス・インターフェース定義、 メンバ定義またはステートメントに限られます。
#+ はネストすることも可能です。
複数のモジュールを一度にコメントアウトしたい時のために、 module を { } で囲めるようになっています。 複数のモジュールを { } で囲んでも、プログラムとしての意味は変わりません。 (スコープを制限したりモジュールをグループ化したりする効果は一切ないので 注意してください。)
#+ は、 // を前に書くことで、無効にすることができます。 一時的にコメントアウトの効果をはずす時に便利です。
これは、 #+comment を使ったプログラム例です。
module t.sharpPlus.m1 { class SS { void main(String[] args){ original(args); #+comment System.out.println("aaa"); //#+comment { System.out.println("bbb"); } System.out.println("ccc"); } } } #+comment { module t.sharpPlus.m1{ #+comment class C { } } #+comment module t.sharpPlus.m2{ } }
上のプログラムは、結局、次のプログラムと等価です。
module t.sharpPlus.m1 { class SS { void main(String[] args){ original(args); { System.out.println("bbb"); } System.out.println("ccc"); } } }
実装の都合上、 #+comment <Statement> は、 ";" (すなわち空文)に変換されます。 return 文などの後ろにこれを書くと、 jikes コンパイラでは、 "This statement is unreachable." となるので 注意してください。
#+ 構文は、 C 言語の#ifdef のような条件コンパイルの機構としても使えます。
mjc -J-Ddebug=trueというオプションを付けることで、 #+debug 以降の構文は有効に、 #-debug 以降の構文は無効になります。
モジュール間の「extends する」という関係にはサイクルが許されません。 しかし、特にプログラム開発の初期の段階ではどうしても、 モジュール間の相互依存が生じます。 そこで導入されたのが uses 宣言です。
uses 宣言は、 extends 宣言と同様に、名前空間の継承を行いますが、 参照関係のサイクルが許されます。
その代わり、 uses 宣言を使うと分割コンパイルはできなくなります。 例えば、次のプログラム例で、2つのモジュールがそれぞれ A.java, B.java というファイルに書かれているとします。
module t.uses.a uses t.uses.b { define class A { define A(){} define B getB(){ return new B(); } } } module t.uses.b uses t.uses.a { define class B { define B(){} define A getA(){ return new A(); } } }
このプログラムをコンパイルするには、 A.java, B.java を、 mjc の引数に同時に与えなければなりません。 (そうしないとコンパイルエラーになるか、古いコンパイル結果を 参照してコンパイルすることになります。)
extends 宣言は、モジュールを linearize するときの上下関係の制約を与えますが、 uses 宣言は、与えません。 extends 宣言と uses 宣言の違いは、これだけです。
モジュール a で定義された名前(クラス・インターフェースまたはメソッド)を モジュール b で拡張する場合は、 linearized list で b は a よりも下に来ないと、正しいプログラムにはなりません。 したがって、この場合は b uses a ではなく、 b extends a と宣言しなければなりません。
それ以外の場合、つまり a で定義された名前を b で利用しているだけならば、 b uses a と宣言してもかまいません。
MJ でも Java 同様にクラスの dynamic loading をすることが可能です。 次のプログラムは、dynamic loading の例です。
module t.forName { define class A { static int x = 123; } class SS { void main(String[] args){ original(args); try { Class c = Class.forName("t.forName.A"); A a = (A)c.newInstance(); System.out.println(a.x); // 123 } catch (Throwable e){ e.printStackTrace(System.err); System.err.println(e.getMessage()); throw new Error(); } } } }
なお、現在の実装では c.newInstance(); を実行したときに、 クラス A のコンストラクタは呼ばれないので注意して下さい。 メソッド _init_A を明示的に呼び出す必要があります。
大規模なアプリケーションでは、一般に名前の衝突が深刻な問題となります。 MJ では特にモジュールの多重継承によって、 別々に定義されれた名前が、衝突する可能性があります。
Java 言語では、 import 宣言で生じるクラス名の衝突を解決するために クラスの完全限定名を使用します。 MJ はこのアイデアを、フィールド・メソッド名にも適用することで、 名前の衝突の問題を解決しています。
MJ では、 ソースコード中のある地点において、2つ以上の foo という名前が参照可能な時、 もしその地点で foo という「単純名」を使ったら、 ambiguous error になります。 MJ では、名前の shadow (hide とも言います) は決して起きません。 ambiguous error が起きた場合、プログラマーは「完全限定名」を 指定することで必ずそのエラーを回避することができます。 この原則は、すべての名前(型名、フィールド名、 メソッド名、ローカル変数名)で徹底されています。
(Java は、名前の衝突の扱い方が非常に不統一です。 例えばローカル変数は外側のローカル変数を shadow できないのに、 ローカル変数でフィールドを shadow することはできます。 また、 interface のフィールド(定数)は、多重継承によって名前が衝突しても 別々の定数として扱われますが、 interface のメソッドは、 signature が同じならば同一のメソッドとして扱われます。)
完全限定名は「その名前を最初に定義したモジュール名 m 」と「単純名 n」の組で 表され、 MJ のソースコード上では、 FQN[m::n] と表記します (FAQ 参照)。 メソッド完全限定名は、メソッド宣言とメソッド呼び出しの両方で 使うことができます。次の例を見てください。
module t.fqn.m1 { define class A { define A(){} } } module t.fqn.m2 extends t.fqn.m1 { class A { define int m(){ return 2; } } } module t.fqn.m3 extends t.fqn.m1 { class A { define int m(){ return 3; } } } module t.fqn.m4 extends t.fqn.m2, t.fqn.m3 { class A { int FQN[t.fqn.m2::m](){ return original() * 10; } int FQN[t.fqn.m3::m](){ return original() * 100; } } class SS { void main(String[] args){ original(args); A a = new A(); //System.out.println(a.m()); // error System.out.println(a.FQN[t.fqn.m2::m]()); // 20 System.out.println(a.FQN[t.fqn.m3::m]()); // 300 } } }
上のプログラム例で、もし完全限定名を使わずに a.m() というふうに あいまいなメソッド呼び出しを行うと、次のようなエラーになります。
% mjc Tutorial.java Preprocessing phase. Tutorial.java:322: MJ: Reference to m() of t.fqn.m1.A is ambiguous. : t.fqn.m2::m t.fqn.m3::mSystem.out.println(a.m()); // error ^ 1 errors
MJ では、複数のモジュールを組み合わせたとき、実装欠損という 現象が起きる場合があります。 欠損を埋めるためのモジュールが補完モジュールです。
実装欠損について、詳しく説明しましょう。 次のプログラムを考えます。
module t.compl.orig { define abstract class S { } define class A extends S { } } module t.compl.sub extends t.compl.orig { define class B extends S { } } module t.compl.abst extends t.compl.orig { class S { define abstract int m(); } class A { int m(){ return 1; } } } module t.compl.compl_sub_abst complements t.compl.sub, t.compl.abst { class B { int m(){ return 2; } } }
t.compl.orig は、 abstract なクラス S とそのサブクラス A を定義しています。 t.compl.sub は、新たなサブクラス B を定義しています。 一方、 t.compl.abst は、クラス S に abstract method m を定義し、 サブクラス A でそれを実装しています。
t.compl.sub と t.compl.abst は、いずれもリンク時エラーを起こさない、 完全なプログラムです。 ところが、この両方のモジュールを同時に使おうとすると、 次のようにリンク時エラーになります。
% mj -s t.compl.sub t.compl.abst MJLinker: non-abstract class t.compl.sub.B has an abstract method : abstract int t.compl.abst::m()
つまり、クラス B のメソッド m を誰も実装していないために、 エラーになります。 このように、正しく動く複数のモジュールを組み合わせたとき、 実装されていない abstract method が発生する現象を「実装欠損」と呼んでいます。 一般に実装欠損を自動的に埋めることは不可能です。 誰かが仕様を理解して、実装欠損を埋めるモジュールを実装しなければなりません。 このようなモジュールを補完モジュールと呼びます。
MJ は、エンドユーザが、実装の詳細を知らなくても、 モジュールを組み合わせて自分の好みのアプリケーションを構築できることを 目標としています。 エンドユーザにとっての使い勝手を良くするために、 MJ のリンカーは、補完モジュールの自動リンクの機能を持っています。
t.compl.sub と t.compl.abst の間を補完する 補完モジュールは、 "complements t.compl.sub, t.compl.abst" という complements 宣言を持つモジュールとして定義します。
例えば誰かが次のような補完モジュールを実装して、 CLASSPATH を通して見える場所に置いたとします。
module t.compl.compl_sub_abst complements t.compl.sub, t.compl.abst { class B { int m() { return 2; } } }
ここで、 complements 宣言は、コンパイラにとっては、 extends 宣言と全く同じ意味です。 ただし、このモジュールが補完の対象とするモジュールの情報が、 コンパイル結果のクラスファイルに付加されます。
そして、エンドユーザが次のように t.compl.sub と t.compl.abst を 組み合わせようとすると、リンカーは、 CLASSPATH の中から 補完モジュール t.compl.compl_sub_abst を見つけ、 それもリンクの対象とします。
% mj -s t.compl.sub t.compl.abst
つまりエンドユーザにとっては補完は自動的に行われるため、 実装欠損の問題を意識しなくてもすみます。
Last updated:
May 20 16:47:37 2002
|