Hatena::ブログ(Diary)

プログラマーの脳みそ このページをアンテナに追加 RSSフィード

2015-12-25

セミコロンレスJavaコンパイラの設計

00:01 |  セミコロンレスJavaコンパイラの設計を含むブックマーク

すいません。完成まで漕ぎ着けれませんでした。

本稿はセミコロンレスJava Advent Calendar 2015の25日目です。

セミコロンレスJavaについて簡単に説明すると、Javaの構文上のセミコロンを用いないで標準APIの範囲でプログラミングをするにはどうしたら良いかを考える衒学的な娯楽です。

アプローチ

Oracle版のJDK (Java Development Kit)に標準で入っているcom.sun.tools.javac.main.JavaCompilerを使います。JDKインストールするとtools.jarというのがJavaインストールフォルダ以下に展開されていると思います。Windowsだと C:\Program Files\Java\jdk1.8.0\lib あたりですね。

このtools.jarに含まれているJavaCompilerを使うとJavaソースコードコンパイルしてAST(abstract syntax tree)抽象構文木を得ることができます。抽象構文木は言語の構文を木構造ノードで現したもので、この抽象構文木GoFのVisitorパターンをつかって走査して、それを元にセミコロンレスJavaのコードを吐き出す、という目論見です。

厄介なのは、識別子の解決で、import文とクラス名から対象のクラスの完全名を当てたりするところですね。まだコンパイルしていないコードに存在しているクラスとかもあるので、まずは一回目のVisitorで識別子の一覧を作って、二回目のVisitorで実際の処理を行うような工夫が必要です。

古いC言語のヘッダファイルのように、宣言だけくくりだしてあるとコンパイルの処理が楽になります。一回中間コード(オブジェクトファイル)に置き換えてから識別子の接続をすればメモリも食わずに済む。このあたりが古いC言語の時代のコンパイラとリンカの存在理由なのかなと思います。

でもソースコードを書く側は面倒くさいですよね。Javaの時代にはCPUパワーがやや向上しましたしメモリも増えてきていましたから、ヘッダファイルなし、中間コードの生成なしにコンパイルしてくれるようになりました。便利です。

クラスの一般セミコロンレス化

セミコロンレスJava技法はいろいろと工夫されていますが、コンパイラを作ろうとする場合、一般化が大事です。よりよいコンパイラ最適化を行うでしょうが、まずは一般化してコンパイル出来るようにすることが重要です。

例として以下の様なクラスを検討しましょう。

public class HelloObject {
	/** フィールド */
	public String name;

	/** コンストラクタ */
	public HelloObject(String name) {
		this.name = name;
	}

	public void syso() {
		System.out.println(hello());
	}

	public String hello() {
		return "hello "+name;
	}

	public String hello(String message) {
		return "hello "+name+", "+message;
	}
}

フィールドがあって、コンストラクタがあって、インスタンスメソッドが3つある、普通のJavaのクラスです。しかし、セミコロンレスJavaをかじった人であれば、Javaのクラスそのものをクラスの機能を保ったままセミコロンレス化することが困難であることは容易に想像が付くことでしょう。

  • フィールドの宣言が出来ない
  • 戻り値のあるメソッドの宣言が出来ない (return文が書けないため)

というのはよく知られたところであります。

上記クラスをセミコロンレス化すると以下の様な形になります。以下は読みやすいようにimport文は残したままにしています。

import java.util.HashMap;
import java.util.LinkedList;
import java.util.stream.Stream;

@SuppressWarnings("serial")
public class HelloObjectSlj extends HashMap<String, Object>{
//	public String name;

	/** コンストラクタ */
	public HelloObjectSlj() {}
	public HelloObjectSlj(String name) {
		if (this.put("name", name) == null) {}
	}

	public void syso() {
		try (Stream<?> stream = Stream.of().onClose(()->System.out.println(new m0_hello().pop()))) {}
	}

//	public String hello() {
	public class m0_hello extends LinkedList<String>{
		public m0_hello() {
			try (Stream<?> stream = Stream.of().onClose(()->m0_hello(this))) {}
		}
	}
	public void m0_hello(LinkedList<String> ret) {
		if (ret.add("hello "+ (String)get("name"))){}
	}

//	public String hello(String message) {
	public class m1_hello extends LinkedList<String>{
		public m1_hello(String message) {
			try (Stream<?> stream = Stream.of().onClose(()->m1_hello(this, message))) {}
		}
	}
	public void m1_hello(LinkedList<String> ret, String message) {
		if (ret.add("hello "+ (String)get("name") +", "+message)){}
	}
}

クラスの宣言

セミコロンレスJavaではフィールドの宣言が出来ませんし、戻り値の返るメソッドの宣言が出来ませんから、一般にクラスを用いたオブジェクト指向プログラミングは不可能であると考えられているかと思います。そのため、どうしてもセミコロンレスJavaで実装されるコードは小規模にとどまっていました。

プログラミングの歴史を辿れば、オブジェクト指向によるコードのモジュール化は、大規模なコードを整理整頓するために役立ち、巨大なシステム開発の土台となりました。

まずはフィールド宣言ですが、フィールドの宣言が出来ないためHashMap<String, Object>を継承することでフィールドに代えます。フィールド名をキーにして値をputすることでフィールドへの格納とし、フィールド名をキーとしたgetでフィールドの参照に代えます。

HashMapを継承するとjava.io.Serializableをimplementsすることになるため、EclipseなどIDEが警告を出すかと思います。そのため@SuppressWarnings("serial")をつけています。詳しくはEffectiveJavaあたりを参照して欲しいのですが、直列化のバージョンID (serialVersionUID) を宣言することで解消するのが筋なのですけれども、そのためにはフィールド宣言が必要になるためセミコロンレスJavaでは無視するより仕方がないのです。

コンストラクタ

コンストラクタはそのまま定義することが出来ます。愛しいですね

ただし、諸般の事情デフォルトコンストラクタは必ず用意しておく必要があります。

戻り値のないメソッドの宣言と文の実行

戻り値のないメソッドの宣言はそのまま行うことが出来ます。

メソッド中の式(要するに値として評価されるもの)の記述は

if ((/* 式 */)==null) {}

というif文を用いる記法が一般的かと思います。

困るのは文で、いろいろな工夫がされて来たのですが、Java8ではラムダ式を用いて

Runnable r;
if ((r = ()->System.out.println("hello!"))==null) {}

といったようにRunnableとなるラムダ式を用いれば文末のセミコロンを取り除くことが出来ます。

しかし、宣言されたRunnableのrun()メソッドを呼び出す場合、戻り値がvoidですから呼び出しに困ります。

Threadを作って実行すると同一スレッドで順次処理出来ないので甚だ不便です。

そこでExecutors#callable()を用いることで戻り値のあるCallableに変換した後、Callable#call()を呼び出すという手法を使っていましたが、この技法の欠点はCallable#call()がthrows Exceptionであるという点です。

これを克服するために開発された技法がStreamの(正確にはその親であるBaseStreamの)onClose()を用いる方法です。

try (Stream<?> stream = Stream.of().onClose(()-> /* 文 */ )) {}

StreamのonClose()は、引数として渡したRunnableを、まさにStreamのclose()メソッドが呼び出された時に実行するわけですが、このclose()はAutoCloseableから継承されたclose()ですので、try-with-resources構文で呼び出すことが出来ます。

余計なthrowsもなく、(他スレッドではなく)同期的に実行できる点でこの技法は優れています。

戻り値のあるメソッドの宣言

戻り値のあるメソッドはreturn文が書けないため宣言できないというのがセミコロンレスJavaでの常識でありました。克服するための技法としては第一引数に値を返せる参照をもらって副作用をもたらし呼び出し元に値を伝えるという技法が考えられます。

例えば配列を使って

public void zzz(String[] ret) {
	if ((ret[0] = "zzz") == null){}
}

といった形で値を返す、というわけなのですが、このメソッド宣言では呼び出し側が甚だ使いにくいわけなのです。

for(String[] s : new String[][]{new String[1]}) {
	try (Stream<?> stream = Stream.of().onClose(()->zzz(s))) {}
	try (Stream<?> stream = Stream.of().onClose(()->System.out.println(s[0]))) {}
}

zzz(new String[1]) という形でnewして渡しっぱなしにすると戻り値が取れないんですね。なのでいったん変数を宣言して、zzz(s)と呼び出して、そこから連鎖的に呼び出しができないので改めてSystem.out.println(s[0])とするような…。

こんなの、コンパイルの一般規則として使えないじゃないですか。

そこで古くよりjava.util.LinkedList継承した内部クラスや匿名クラスのコンストラクタを用いてthisに処理をaddさせ、newして出来たオブジェクトからpop()して用いるということが行われてきたわけです。コンストラクタが愛しいですね。

ところが、ただ内部クラスにしてしまうと困ったことが起こるのです。

ポリモフィズム

単に内部クラスとして

public class HelloObjectExSlj extends HelloObjectSlj {
//	public String hello() {
	public class m0_hello extends LinkedList<String>{
		public m0_hello() {
			if (add("こんにちは "+ (String)HelloObjectExSlj.this.get("name"))){}
		}
	}
}

というようにすると、ポリモフィズムがされず、宣言型によって静的にHelloObjectSlj.m0_hello型とHelloObjectExSlj.m0_hello型が呼び分けられるようになってしまうんですね。

例えば以下のようにh型と、実体が違う組み合わせを実行すると

// 親の型
for (HelloObjectSlj h : new HelloObjectSlj[]{new HelloObjectSlj("なぎせ")}){
	try (Stream<?> stream = Stream.of().onClose(()->
		System.out.println(h.new m0_hello().pop()) )) {}
}
// ポリモフィズムのつもり
for (HelloObjectSlj h : new HelloObjectExSlj[]{new HelloObjectExSlj("なぎせ")}){
	try (Stream<?> stream = Stream.of().onClose(()->
		System.out.println(h.new m0_hello().pop()) )) {}
}
// 子の型
for (HelloObjectExSlj h : new HelloObjectExSlj[]{new HelloObjectExSlj("なぎせ")}){
	try (Stream<?> stream = Stream.of().onClose(()->
		System.out.println(h.new m0_hello().pop()) )) {}
}

hello なぎせ

hello なぎせ

こんにちは なぎせ

といった結果になり、2つめのケース、HelloObjectSlj hで変数が宣言され、new HelloObjectExSlj()としている、要するにポリモフィズムして欲しいパターンでポリモフィズムしないわけなんです。内部クラスのnew演算子は静的に決められてしまう。

そこで、呼び出しやすいように内部クラスを宣言しつつ、実装は普通のメソッドにしてそちらを呼び出すようにしておく

//	public String hello() {
	public class m0_hello extends LinkedList<String>{
		public m0_hello() {
			try (Stream<?> stream = Stream.of().onClose(()->m0_hello(this))) {}
		}
	}
	public void m0_hello(LinkedList<String> ret) {
		if (ret.add("hello "+ (String)get("name"))){}
	}

class m0_helloをnewすると、その内部から m0_hello(LinkedList<String>)が呼び出されるわけです。このような仕込みをした上で、次のように子クラス側でオーバーライドをしてやる

@SuppressWarnings("serial")
public class HelloObjectExSlj extends HelloObjectSlj {
	/** コンストラクタ */
	public HelloObjectExSlj() {}
	public HelloObjectExSlj(String name) {
		if (this.put("name", name) == null) {}
	}
	@Override
	public void m0_hello(LinkedList<String> ret) {
		if (ret.add("こんにちは "+ (String)get("name"))){}
	}
	@Override
	public void m1_hello(LinkedList<String> ret, String message) {
		if (ret.add("こんにちは "+ (String)get("name") +", "+message)){}
	}
}

このようにすることで、

hello なぎせ

こんにちは なぎせ

こんにちは なぎせ

といったように、正しくポリモフィズムさせることが出来るようになるのです。

そうそう、コンストラクタのところでデフォルトコンストラクタを用意する諸般の事情というのは継承をした時に引数付きのsuper()を呼び出せないという事情です。継承しないと気が付かないところですね。

まとめ

本稿では一般的なJavaオブジェクトをセミコロンレス化させる技法について検討しました。それにより

ということを示しました。

こうした一般化規則によってセミコロンレスJavaオブジェクト指向プログラミングが可能であることを示しました。

おまけ

抽象構文木をVisitorで走査する場合、com.sun.source.tree.TreeVisitorインターフェースを直接implementsせずcom.sun.source.util.TreeScannerクラスを継承して実装するのが一般的かと思いますが、既存クラスの継承をして戻り値のあるメソッドをオーバーライドすることがセミコロンレスに行えないのでcom.sun.source.tree.TreeVisitorインターフェースをimplementsすることになります。

この時、java.lang.reflect.Proxyを用いて動的にインターフェースの実装を作り処理をInvocationHandlerに委譲させます。InvocationHandlerは関数インターフェースなのでラムダ式で記述することができ、戻り値を返す実装をすることができます。

うまく実装を書いてやれば、セミコロンレスJavaコンパイラによってセミコロンレスJavaコンパイラ自身のソースコードコンパイルすることが出来るようになるでしょう。

匿名匿名 2016/01/10 22:14 Minecraftもコレで動いてるんですね

トラックバック - http://d.hatena.ne.jp/Nagise/20151225/1450969314