MJ の言語仕様に関する質問

MJ はアスペクト指向言語なのですか?

MJ は広い意味でのアスペクト指向言語です。 複数のクラスを横断するコードを、別のモジュールに分離して記述できます。

しかし、 MJ は代表的なアスペクト指向言語である AspectJとはかなり考え方が異なります。 AspectJ はデバッグ、同期、性能向上など、プログラム本来の機能とは 異なるアスペクトを分離することに主眼を置いていますが、 MJ はそれらは重視していません。

MJ が解決しようとする問題は、 どちらかといえば、Hyper/J に近いものです。 従来の手続き指向言語あるいはオブジェクト指向言語は、 1種類のモジュールの切り分け方(手続きまたはクラス)を プログラマーに強制するものですが、 Hyper/J や MJ は自由な切り分け方を可能にするシステムです。

また、MJ は、言語仕様のシンプルさと、モジュール結合時の安全性を 第一に考えて設計しています。この点は、他の AOP システムと大きく異なる点です。

「差分ベースモジュール」は Java 言語以外に適用できるのか?

オブジェクト指向言語でなくても、手続き型言語、 論理型言語、ハードウエア記述言語などにも適用できると思います。 多くのプログラミング言語は、なんらかの記述単位に「名前」を付けます。 MJ のモジュール機構のアイデアは、オリジナルのプログラムの「名前」に 対応付けられたコードに、なんらかの形で差分を追加する、 というシンプルなものです。 したがって、多くのプログラミング言語に適用することができます。

例えば、差分ベースモジュールを emacs-lisp に適用したものが、 dm-elisp です。

MJ のモジュール機構を適用できない言語もあります。 例えば項書換え系は、記述単位に名前を付けないので、適用できません。

モジュールの多重継承ができるのは好ましくないのでは?

多重継承の問題のうち、名前の衝突に関しては、 フィールド・メソッドの完全限定名の導入によって完全に解決しています。

もう一つの問題、意味的衝突については、 Design by Contract および behavioral subtyping の考え方を差分ベースモジュールに応用することで 解決できます。(詳しくは現在論文執筆中です。)

しかし、 tree 構造の方が一般のグラフ構造よりも人間にとって 扱いやすいことは確かです。 MJ におけるプログラミングでも、 複雑なモジュールの多重継承を推奨しているわけではありません。 モジュールの継承グラフは、できるだけシンプルな形になるように プログラミングした方がよいと思います。

理想的なモジュールの継承グラフは、継承の深さが浅く、 横に広い形をした継承グラフです。 継承グラフがそのような形をしていれば、プログラムの保守が容易になります。

MJ のモジュールと Java の package との違いは?

Java には class という構文とは別に package がありますが、 役割分担が完全にできていません。 package も class も、ともに名前空間の役割を果たしています。 MJ は、クラスの役割とモジュールの役割を整理して分けることで、 言語仕様を Java よりもシンプルにしています。

拡張性・再利用性を高くしようとすると、モジュール分けに時間がかかってしまうのでは。

XP が言っているように、最初から拡張性・再利用を考えたモジュールの切り分けに 時間を割くことはしない方がよいと思います。

この言語に限らず、拡張性・再利用性を高くするには労力が必要です。 先々のことを考えて拡張性・再利用性の高いモジュール分けを行っても、 実際に拡張や再利用が行われなければ、その苦労は報われません。 プログラマは、そのトレードオフを考えて時間配分を行わなければなりません。

OOP のいいところは、 記述対象をクラスにモデル化して記述さえすれば、 それだけで、自動的にある程度拡張性・再利用性の高いプログラムができる という点です。 つまり、「クラス=モジュール」は、近似的には成り立つ場合が多いので、 クラスの設計さえすれば、それだけで、 そこそこよいモジュール分けができてしまうわけです。 これもオブジェクト指向パラダイムのメリットの1つだと思います。

このメリットは、 MJ にも引き継がれています。 完璧なモジュール分けに頭を悩ます時間がなければ、 従来言語と同様、1クラス1モジュールで書いておけばいいのです。 それだけで、そこそこのモジュール分けは達成されているはず。 開発・保守が進んで行くうちに問題ができたら初めて、 モジュールの分け方を見直せばよいのです。

モジュールの分け方を変更する作業は リファクタリングと呼ばれています。 将来的には MJ でのリファクタリングをサポートするツールができれば 便利だと思います。

この言語で書いても、モジュラリティの悪いプログラム、拡張性の悪いプログラムは書けるのでは?

そのとおりです。 オブジェクト指向設計とリファクタリングは、 MJ でのプログラミングでも重要です。

1つのクラス定義が複数のモジュールに切り刻まれると読みにくいのでは?

同様の批判が、従来のオブジェクト指向言語にも言われていたことを 思い出して下さい。 「1つのクラスの定義が、スーパークラスとサブクラスに分離されているため、 プログラムが読みにくい」という批判です。 この批判に対する答えは2つあります。

1つは Design by Contract の実践です。 個々のクラスやメソッドの外部仕様を明確にしておけば、 そのクラスやメソッドの内部実装のコードをいちいち見に行く必要がありません。 つまり内部実装が複数の場所に分散していても問題にはなりません。 むしろ、情報隠蔽によって不必要なクラスやメソッドの名前が 見えなくなることにより、プログラムの可読性は増すことになります。 この答えは、差分ベースモジュールにもそのまま当てはまります。

もう1つは、 UML などを使った外部ドキュメントです。 オブジェクト指向言語が扱う問題は従来の言語が扱う問題よりもはるかに複雑で、 もはやソースコードを読むだけで全体像を理解することは不可能です。 そこで、 UML などを使った理解が必要になります。 この答えも、差分ベースモジュールにそのまま当てはまります。 UML のクラス図を差分ベースモジュール用に拡張した、 レイヤードクラス図を使えば、モジュールがプログラムを拡張する様子を 視覚的に分かりやすく表現することができます。

クラスの多重継承はサポートしないのか?

クラスの多重継承はオブジェクト指向言語に必要な機能だと思います。 しかし、 MJ が JavaVM の上で実行される言語である限りは、 クラスの多重継承をサポートをする予定はありません。 現在の JavaVM 上でクラスの多重継承を実装すると、 どうしても実行速度が遅くなります。

名前空間の import と inheritance は別の概念であり、分けるべきでは?

Szyperski, C.A.: "Import is Not Inheritance, Why We Need Both: Modules and Classes", In Proc. of ECOOP'92.
という論文では、 クラスの継承をライブラリの import の変わりに使うべきではないと 主張しています。

しかし、本来は異なる概念を1つの言語機構で実現することは、 プログラミング言語を設計する上でよく行われます。 例えば、 Java では class を情報隠蔽の単位、型定義の機構、排他制御の単位、 メモリの動的割当の単位などに用いています。 このように1つの言語機構に多くの機能を unify することで、 言語の多機能さと簡潔さという矛盾する要求を同時に満たすことが可能となります。

MJ は、クラスから名前空間の機能を分離する代わりに、 名前空間と差分プログラミングの機構を unify しました。 この試みは成功しているようです。

import と inheritance は、もともと機能的に良く似ています。 Eiffel 言語の文化では、継承機構を積極的に「ライブラリの取り込み機構」として 利用しています。 例えば、クラスの中から標準入出力のライブラリを使う時には、 その機能を提供するクラスを継承します。 (ただし、 Eiffel ではライブラリのインターフェースは、 暗黙のうちに継承されることはありません。)

public/private がなくなったが、セキュリティメカニズムはどうなる?

sandbox の内側で動作する applet や、 セキュリティに無関係なスタンドアロンの application を作るならば、 MJ は、普通の Java と全く同様に使うことができます。

「sandbox の外側」、つまり「安全でないコードを動的にロードして実行する アプリケーション」の記述には、現在の MJ は使えません。 セキュリティの観点からは、 MJ で定義されたクラス名・フィールド名・メソッド名はすべて public であると 思ってください。

多重継承における名前の衝突を rename で回避することはできないのか?

名前の衝突の問題は、メソッド完全限定名の導入により 解決できています。 しかし、メソッド完全限定名だけでは解決できない種類の衝突があります。 例えば、 module m1 で定義されている abstract method foo を、 m1 の sub-module である m2, m3 でそれぞれ実装していて、 m2, m3 を同時に継承する sub-module m4 があるという状況を考えます。 このとき、 m4 から見ると、 m2 か m3 のいずれかの foo の実装が、 override によって消えてしまう、 という問題があります。

もし m4 が、 m2 と m3 の両方の foo の実装を生かしたいと思ったら、 Eiffel 言語のようなメソッドの rename の機構が必要になるでしょう。 この機構については、将来、導入したいと思っています。

FQN のシンタックスがひどいと思う。

現在のようなシンタックスになっているのは、 Java 言語の「パッケージ名の区切りにもメンバーの参照にも "." を使っている」 という問題に起因します。 この問題が解決できれば、もっとまともなシンタックスにできると思います。 ( XML のように名前空間の別名を prefix に使うようにすれば、 シンタックスがすっきりするでしょう。 これは将来のバージョンで実装しようと思います。)

MJ では依存する名前空間を最小にできるので、 名前の衝突が起きる確率は従来のオブジェクト指向言語よりも かなり低くなると予想しています。 実際のプログラミングの際に、プログラマが FQN を指定しなければ ならなくなることは、あまりないと思います。

m1 を m2 が extends すると、 m1 内部で定義された field が m2 から見えてしまう。 これでは情報隠蔽にならないのでは?

MJ において「 m1 を m2 が extends する」ということは、 Java の nested class で表現すれば「クラス m1 の内部に m2 が定義されている」に 相当する、と理解してください。 内側からは、外側で定義されているすべての名前にアクセスできます。

MJ では、すべての名前のアクセス修飾子は "protected" である、 と見ることもできます。 普通の Java で super class の protected field が subclass から見えるのと同様、 MJ では super-module の中のすべての名前が、 sub-module から見えます。 そのかわり、 m1 を extends するすべてのモジュールのプログラマは、 m1 の利用者ではなく、実装者の立場になることを意識して プログラムしなければなりません。

現在の言語仕様にはありませんが、将来的には "final module" のような 機構を入れるかも知れません。これは Java の final class のように、 sub-module の定義を許さないモジュールです。 final module の内部で定義される名前はすべて "private" である、と言えます。

final module 機構は、foolproof のための機構です。 あるプログラマーが、 クラスの外部インターフェースだけでなく内部実装にも依存したプログラムを書くと、 ペナルティを受けるのは明らかにそのプログラマーです。 将来そのクラスの内部実装が変更になった時に、 そのプログラマーは自分が書いたプログラムを書き直さなければなりません。

将来的には Eiffel のような assertion 機構を MJ に入れるつもりですが、 それが実現されれば、実装に依存するプログラマーは、 さらにペナルティを受けることになります。 クラスの外部インターフェースのみを利用するプログラマーは 事前条件だけを満たせば良いのですが、 実装に依存するプログラマーは、不変条件と事後条件も 満たすようにプログラムしなければなりません。 つまり、より制約の強いプログラミングを強いられることになります。

c extends a, b と書いたとき、 a と b のどちらが上に来るかが決まっていないのは困るのでは?

CLOS などの言語では、 c extends a, b と宣言すると、 a が b よりも優先されると解釈されます。 これを local precedence order と呼びます。 これらの言語では linearize の際には、local precedence order も保存されます。

MJ では、エンドユーザがモジュールを組み合わせるときに、 できるだけリンク時エラーが起きないようにしたいと考え、 現在は local precedence order を言語仕様に入れていません。 次のような例を考えましょう。

module c extends a, b {...}
module d extends b, a {...}

エンドユーザが c と d のモジュールを組み合わせる場合、 local precedence order を保存するような linearize は不可能です。 CLOS などでは、このような場合はエラーになります。

リンク時エラーの原因になるというデメリットを上回るメリットがあれば、 将来は、 local precedence order を言語仕様に入れるかもしれません。

モジュールとクラスを分けたのはいいが、実装の継承とインターフェースの継承(subtyping)も分けるべきである。

実装の継承とインターフェースの継承が別の概念であることは確かですが、 言語機構として別のものを用意すべきだという意見には賛成できません。 subtype は super type と振る舞いを共有する場合が多いので、 プログラマーの便宜を図るためには、 subtype を作ったら自動的に 実装も継承される方がよいと思います。

また、2つの機構を用意するよりも 1つの機構(クラスの継承機構)ですませれば、 言語仕様が単純になるという大きな利点もあります。

確かに、「実装は継承したいが、 subtype にはなりたくない」 「subtype を作りたいが実装は継承したくない」という場合もあると思います。 前者の場合については、 C++ の private inheritance のようなものがあるとよいと思います。 後者の場合については、あらかじめフレームワークの実装者が 仕様モジュールと実装モジュールを分けて定義しておくのが、 そもそも望ましいと思います。 もしフレームワークがそういう風にかかれていなかったとしても、 すべてのメソッドを override することで、 super class の実装を無視することが できます。

sub-module は super-module の内側にあるモジュールなのでしょうか?外側にあるモジュールなのでしょうか?

これは、モジュール間のどういう関係に着目するかで、 答えは違って来ます。

Java の nested class のイメージで言うなら、 sub-module は super-module の内側にあります。 sub-module からは super-module のすべての名前にアクセスできますが、 これは nested class において、 inner class が outer class の すべての名前にアクセスできることに相当します。 一般に、外側のモジュールは抽象的な外部インターフェースであり、 内部のモジュールは、より具体的な実装です。 つまり、「内部を隠蔽するという関係」で言えば、 super-module は sub-module を 隠蔽しています。

これは実世界のモジュールの入れ子関係の対比でも理解できます。 例えば、パソコンのケースの内部にはハードディスク、マザーボードなどの モジュールがあり、それぞれはさらに細かいモジュールから構成されています。 外部のモジュールほど、内部を隠蔽して、 抽象度の高い機能を、外部のユーザに提供します。 このように、 super-module と sub-module は、それぞれ 実世界における外側のモジュールと内側のモジュールに対応します。

ところが、「随伴関係」と「依存関係」に着目すると、 MJ のモジュールと実世界との対応は違ってきます。

実世界のモジュールでは、 外側のモジュールの場所を移動すれば、同時に内部のモジュールも移動します。 つまり、内部のモジュールは外部のモジュールに随伴します。 一方 MJ では、 リンク時に sub-module を指定すると、 super-module も自動的にリンクされます。 つまり、 super-module が sub-module に随伴するのです。 これはモジュールの入れ子関係とは逆転しています。

最後に「依存関係」という観点で見ると、どうでしょうか。 実世界のモジュールで、例えばマザーボードというモジュールの中に、 CPU というモジュールがあります。 マザーボードとCPU は、どちらがどちらに依存しているのでしょうか? (ただし、マザーボードも、CPU も、異なるメーカーから互換性のある 複数の製品が利用可能な場合を考えます。) MJ 的な理解では、特定のマザーボードの1製品と、特定のCPUの1製品は、 お互いに全く依存していません。 それぞれを接続可能なのは、「CPU のソケットの規格」を 両方が正しく実装しているからです。 MJ の構文を使うなら、こういうことになります。

module cpuSpec {...}
module motherBoardSpec {...}
module cpuImplementation extends cpuSpec {...}
module motherBoardImplementation extends cpuSpec, motherBoardSpec {...}

この「CPU のソケットの規格」は、 MJ では1つの仕様モジュールとして表現します。 ところが実世界では規格は紙の上の存在であり、 実体を持ったモジュールとしては普通は存在しません。 したがって、 MJ での依存関係は、 実世界のモジュールどうしの関係とは対応しません。

まとめるとこうなります。

隠蔽関係:super-module が外側のモジュール、 sub-module が内側のモジュール

随伴関係:super-module が内側のモジュール、 sub-module が外側のモジュール

依存関係:super-module に sub-module が依存。実世界には対応しない。

parameterized module はないのでしょうか?

現在の MJ にはありません。

既存のクラス階層と、拡張されたクラス階層の両方を1つのアプリケーションで使いたい。

現在の MJ ではできません。 将来は、クラス階層のコピーを作る機構を導入する予定です。

1つの差分を、複数のクラスに追加したい。

現在の MJ では、差分を追加する対象となるクラスのクラス名が 固定であるため、そのようなことはできません。 将来は、差分の追加対象のクラス名を変更するための機構を導入する予定です。

module を動的に追加することはできないのでしょうか。

現在の MJ ではできません。クラスの dynamic loading ならば、 Java 同様にできます。

差分プログラミングの機構としてクラス継承とモジュール継承の両方があるのは冗長で、どちらを使えばよいか迷ってしまう。

クラス継承は is-a 関係がある場合にのみ使うべきであり、 差分プログラミングの道具として安易に使うべきではありません。 この点では MJ は C++ や Java と同じです。 ( Smalltalk は静的型チェックがないせいか、 クラス継承を差分プログラミングの道具として使う文化があるようですが。)

before, after, around advice はないのですか?

今の MJ にはありませんが、必要だと思っています。 次のプログラムは before の代わりになると思う人もいるかもしれません。

module m1 extends m0 {
  class C {
    void foo() {
      m1BeforeFoo();
      original();
    }
  }
}

m1 を書いたプログラマーの意図は、「foo の本体を実行する前に、 doBeforeFoo()を 実行したい」ということでしょう。これは実現可能です。 しかし、これだけでは不十分な場合があります。 別のプログラマーが、 「『自分の super module が追加した foo 本体直前の処理』と 『本来の foo 本体』の間に、処理を追加したい。」と思ったとします。 (実際にこのような要求は起こります。) このプログラマーが次のように記述してもうまくいきません。

module m2 extends m1 {
  class C {
    void foo() {
      m2BeforeFoo();
      original();
    }
  }
}

これでは処理 m2BeforeFoo() は、処理 m1BeforeFoo() の 前に実行されてしまうからです。

この問題は、あらかじめフレームワーク側が、 before hook を用意しておくことで解決可能です。

module m0 {
  define class C {
    define void beforeFoo(){}
    define void foo() {
      beforeFoo();
      defaultFooBehavior();
    }
  }
}

しかし、フレームワーク構築時点では before, after, around のための hook があとで必要になるかどうか予測することはできない場合があります。 また、ひょっとして必要になるかもしれないから言って、すべてのメソッドに before, after, around hook を入れておくことは、現実的には不可能です。 このような理由から、 before, after, around の問題に対処する なんらかの言語機構が必要だと考えています。

なぜ MJ は Java の上位互換の言語にしなかったのですか?

確かに言語研究者は多くの場合、既存の言語の上位互換の形で 新しい言語を提案します。 新しい言語を Java の上位互換にすれば、 Java からの移行も容易ですし、 記述力に関しては少なくとも Java より悪くなることがないことが 保証されるわけです。

しかし、 MJ のモジュール機構のアイデアを、 Java のモジュール機構(package, public/protected/private, nested class)と 整合性をたもったまま取り込むのは極めて困難です。 もし、そのような言語を実際に設計したとしても、 言語仕様が複雑になり、実装も非常に難しく、 実用的言語にすることは不可能だと思います。

「モジュール」は普通、それ単独で何らかの機能を提供するものを言うが、MJ のモジュールは差分であり、コードの塊にすぎないのでは?

sub-module 単独では機能は持ちませんが、 super-module と合わせて考えれば、 機能を持ったクラスの集合を提供します。

これは OOP の subclass と同じことです。 subclass のコードだけでは意味を持ちませんが、 superclass のコードと合わせて初めて、意味のあるクラス定義になります。

拡張モジュールが n 個あると補完モジュールは最大何個必要なのでしょうか?

それはフレームワークがどのように設計されているかによります。 メソッドの追加とサブクラスの追加を許すようなフレームワークでは、 2次元方向の拡張が可能であり、 n 個のモジュールに対して nC2 個、つまり O(n^2) 個の 補完モジュールが必要になります。

フレームワークをうまく設計すれば、 補完モジュールを不要にすることができるでしょう。 その代わり、機能が制限されたり、実行効率が悪くなるかも知れません。

k 次元方向の拡張が可能なフレームワークを設計することもできます。 (例えば、構文の追加、パスの追加、ターゲット言語の追加が可能な コンパイラフレームワークは、3次元方向の拡張が可能なフレームワークです。) その場合は nC3 個、つまり O(n^3) 個の補完モジュールが必要になります。

拡張性について全く考慮されていないフレームワークの場合は、 O(2^n) の補完モジュールが必要になるかも知れません。 これは、 2^n 個のバリエーションを提供するために 2^n 個のプログラムを提供することに相当し、あまりありがたくありません。


mj-logo
Last updated: Aug 27 12:13:30 2002