Edited at

19 オブジェクト生成に対応する

More than 1 year has passed since last update.


はじめに

Javaクラスをロードしてコンストラクタを呼び出し、オブジェクト生成に対応したいと思います。

この記事は「JSON風のオブジェクト定義に対応する」の続きの記事です。


オブジェクト生成対応でやりたいこと

オブジェクト生成対応でやりたいことを確認します。

例えば下のようなプログラムがあります。

loadClass関数を既定のグローバル関数として新たに導入します。

loadClass関数は引数に文字列で、Javaの完全修飾クラス名を渡すとそのクラスをロードします。(1行目)

ロードしたものは変数へ代入します。

その変数にnewを付けて関数のように呼び出すと、インスタンスを生成します。(2行目)

var dateClass = loadClass("java.util.Date")

var date = new dateClass()
println(date.toString())


実装の仕方

実装の仕方について、構文解析(Parser)、インタプリタ(Interpreter)と順に考えていきます。

字句解析(Lexer)の変更はありません。


構文解析(Parser)の実装の仕方

新しく追加された構文はnewなので、newの解析方法を考えます。

newは構文上で1つ目にくるので、他の1つ目のトークンで構文決まるものと同じように、

lead()メソッドへnewの構文を解析する処理を実装します。

newの次にくる構文の要素は関数呼び出しと同じ構文になるので、

newの構文を解析する処理では、次の要素が関数呼び出しであることを確認します。


インタプリタ(Interpreter)の実装の仕方

インタプリタでは新たに導入するloadClass関数と、new構文の実行を実装します。

loadClass関数はすでにある既定のグローバル関数のprintln関数と同様に実装します。

new構文実行の実装は、newの後ろにくる構文を

関数呼び出しのように扱い、インスタンスを生成するようにします。


Javaで実装してみる

実装にうつります。

構文解析(Parser)、インタプリタ(Interpreter)について、

変更と追加をしたところを順にみていきます。


Parser.java

Parser.javaの実装です。

new構文の解析を実装します。

1つ目のトークンで構文が決まるものを実装するlead()メソッドの

// Addのところへnewの構文解析を行うnew_()メソッドの呼び出しを追加しました。


Parser.java

    private Token lead(Token token) throws Exception {

if (token.kind.equals("ident") && token.value.equals("function")) {
return func(token);
} else if (token.kind.equals("ident") && token.value.equals("return")) {
token.kind = "ret";
if (!token().kind.equals("eob")) {
token.left = expression(0);
}
return token;
} else if (token.kind.equals("ident") && token.value.equals("if")) {
return if_(token);
} else if (token.kind.equals("ident") && token.value.equals("while")) {
return while_(token);
} else if (token.kind.equals("ident") && token.value.equals("break")) {
token.kind = "brk";
return token;
} else if (token.kind.equals("ident") && token.value.equals("var")) {
return var(token);
// Add
} else if (token.kind.equals("ident") && token.value.equals("new")) {
return new_(token);
} else if (factorKinds.contains(token.kind)) {
return token;
} else if (unaryOperators.contains(token.value)) {
token.kind = "unary";
token.left = expression(70);
return token;
} else if (token.kind.equals("paren") && token.value.equals("(")) {
Token expr = expression(0);
consume(")");
return expr;
} else if (token.kind.equals("bracket") && token.value.equals("[")) {
return newArray(token);
} else if (token.kind.equals("curly") && token.value.equals("{")) {
return newMap(token);
} else {
throw new Exception("The token cannot place there.");
}
}

new構文解析を行うnew_()メソッドを追加しました。

引数のtokennewトークンが渡されてきます。

インタプリタ処理でのキーになる、newトークンの種類、kindnewにしました。

newトークンのleftexpressionメソッドの戻り値を割り付けます。

その戻り値が関数呼び出しの構文解析になっていることを確認します。


Parser.java

    private Token new_(Token token) throws Exception {

token.kind = "new";
token.left = expression(0);
if (!(token.left.value.equals("(") && token.left.kind.equals("paren"))) {
throw new Exception("No constructor invocation.");
}
return token;
}

Parser.javaの変更は以上です。


Interpreter.java

Interpreter.javaの実装です。

loadClass関数の実体にあたるクラスを追加しました。

invokeメソッドの引数は、loadClass関数呼び出しの引数に当たります。

引数の1つ目が完全修飾クラス名の文字列になっている前提で、

Class.forNameメソッドを使ってクラスをロードします。


Interpreter.java

    public static class LoadClass extends Func {

public LoadClass() {
name = "loadClass";
}

@Override
public Object invoke(List<Object> args) throws Exception {
return Class.forName((String) args.get(0));
}
}


init()メソッドの変更です。

インタプリタを初期化するメソッドです。

// Addのところに追加したLoadClassクラスをインスタンス化して、

既定のグローバル関数として呼び出しできるように、

グローバルスコープのfunctionsへ加えます。


Interpreter.java

    public Interpreter init(List<Token> body) {

global = new Scope();
local = global;
// Add
Func loadClass = new LoadClass();
global.functions.put(loadClass.name, loadClass);
Func f = new Println();
global.functions.put(f.name, f);
this.body = body;
return this;
}

expression()メソッドの変更です。

式を表すトークンの意味(kind)によって分岐する処理です。

// Addの下へオブジェクト生成のためのnew_()メソッド呼び出しを追加しました。


Interpreter.java

    public Object expression(Token expr) throws Exception {

if (expr.kind.equals("digit")) {
return digit(expr);
} else if (expr.kind.equals("string")) {
return string(expr);
} else if (expr.kind.equals("ident")) {
return ident(expr);
} else if (expr.kind.equals("blank")) {
return blank(expr);
// Add
} else if (expr.kind.equals("new")) {
return new_(expr);
} else if (expr.kind.equals("newMap")) {
return newMap(expr);
} else if (expr.kind.equals("newArray")) {
return newArray(expr);
} else if (expr.kind.equals("bracket")) {
return accessArrayOrMap(expr);
} else if (expr.kind.equals("func")) {
return func(expr);
} else if (expr.kind.equals("fexpr")) {
return fexpr(expr);
} else if (expr.kind.equals("paren")) {
return invoke(expr);
} else if (expr.kind.equals("sign") && expr.value.equals("=")) {
return assign(expr);
} else if (expr.kind.equals("unary")) {
return unaryCalc(expr);
} else if (expr.kind.equals("sign")) {
return calc(expr);
} else if (expr.kind.equals("dot")) {
return dot(expr);
} else {
throw new Exception("Expression error");
}
}

オブジェクト生成のためのnew_()メソッドを追加しました。

このメソッドでコンストラクタを呼び出し、生成したオブジェクトを返します。

new_()メソッド引数のexprにはnewトークンが渡されます。

newトークンのleftは関数呼び出し構文になっているトークンが割り付けられています。

関数呼び出し構文になっているnewトークンのleftの、さらにleftは関数名にあたる部分になっています。

newトークンのleftparamsは、関数呼び出し構文の引数にあたる部分になっています。

関数名にあたる部分(expr.left.left)は、コンストラクタ呼び出しではクラスであるため、

処理の始めではその部分をクラスへ解決します。

コンストラクタ呼び出しを行うには、コンストラクタを表す情報を得る必要があります。

コンストラクタの情報はクラスを表す情報から得ることができます。

クラスへ解決されたc変数がそのクラスの情報にあたります。

クラス情報からコンストラクタ情報を得ることができますが、

クラスに複数定義されているコンストラクタのうち、

呼び出したいコンストラクタの情報を1つ選ぶ必要があります。

new_メソッドでは、呼び出したいコンストラクタ情報を1つに選ぶために、

コンストラクタ呼び出しの引数の型情報を使って絞り込みます。

該当するコンストラクタ情報を見つけられないか、1つに絞り込めなかったときはエラーにしています。

絞り込みの仕方は非常に簡便な方法で、Javaコンパイラでの実装とは違っています。

少し詳しい説明をコメント文にしました。


Interpreter.java

    public Object new_(Token expr) throws Exception {

// "("の左側をクラスに解決し、そのクラスのコンストラクタ一覧を取得します
Class<?> c = class_(expression(expr.left.left));
Constructor<?>[] ctors = c.getConstructors();

// "("の右側のコンストラクタ引数を、値へ解決した一覧にします
List<Object> args = new ArrayList<Object>();
for (Token arg : expr.left.params) {
args.add(value(expression(arg)));
}

// 引数から引数の型の一覧を作ります
List<Class<?>> aClasses = argClasses(args);

// 引数の型の一覧が代入可能なシグニチャーになっているコンストラクターのみに絞った一覧にします
List<Constructor<?>> byAssignables = ctorsByAssignable(ctors, aClasses);

// 絞った結果、該当するコンストラクターがなかったらエラー
if (byAssignables.size() == 0) {
throw new Exception("No constructor error");
}

Constructor<?> ctor;
if (byAssignables.size() == 1) {

// 絞った結果、該当するコンストラクターが1つだったら、
// それが呼び出し対象のコンストラクター
ctor = byAssignables.get(0);

} else {

// 絞った結果、該当するコンストラクターが2つ以上だったら、さらに絞り込みます。
// 代入可能なシグニチャーで絞ったコンストラクターの一覧を、
// 引数の型の一覧が完全に一致するシグニチャーになっているコンストラクターのみに絞り込みます。
List<Constructor<?>> byAbsolutes = ctorsByAbsolute(byAssignables, aClasses);

// 絞った結果、該当するコンストラクターが1つにならなかったらエラー
if (byAbsolutes.size() != 1) {
throw new Exception("No constructor error");
}

// 絞った結果、該当するコンストラクターが1つだったら、
// それが呼び出し対象のコンストラクター
ctor = byAbsolutes.get(0);

}

// 1つに絞れたコンストラクターを使って、コンストラクター呼び出しを行う
Object val = ctor.newInstance(args.toArray());
return val;
}

public List<Class<?>> argClasses(List<Object> args) {
List<Class<?>> classes = new ArrayList<Class<?>>();
int psize = args.size();
for (int i = 0; i < psize; ++i) {
Object a = args.get(i);
if (a != null) {
classes.add(a.getClass());
} else {
classes.add(null);
}
}
return classes;
}

public static List<Constructor<?>> ctorsByAssignable(Constructor<?>[] ctors, List<Class<?>> aClasses) {
List<Constructor<?>> candidates = new ArrayList<Constructor<?>>();

int aSize = aClasses.size();
for (Constructor<?> ctor : ctors) {
Class<?>[] pTypes = ctor.getParameterTypes();

if (pTypes.length != aSize) {
continue;
}

Boolean allAssignable = true;
for (int i = 0; i < aSize; ++i) {
Class<?> c = pTypes[i];
Class<?> cc = toBoxClass(c);
Class<?> ac = aClasses.get(i);
if (ac != null) {
Class<?> acc = toBoxClass(ac);
allAssignable &= cc.isAssignableFrom(acc);
}
if (!allAssignable) {
break;
}
}
if (allAssignable) {
candidates.add(ctor);
}
}
return candidates;
}

public static List<Constructor<?>> ctorsByAbsolute(List<Constructor<?>> candidates, List<Class<?>> aClasses) {
List<Constructor<?>> screened = new ArrayList<Constructor<?>>();
int aSize = aClasses.size();
for (int i = 0; i < aSize; ++i) {
Class<?> ac = aClasses.get(i);
if (ac == null) {
return screened;
}
}
for (Constructor<?> ctor : candidates) {
Class<?>[] pTypes = ctor.getParameterTypes();
if (aSize != pTypes.length) {
continue;
}
Boolean allEquals = true;
for (int i = 0; i < aSize; ++i) {
Class<?> c = pTypes[i];
Class<?> ac = aClasses.get(i);
allEquals &= c == ac;
if (!allEquals) {
break;
}
}
if (allEquals) {
screened.add(ctor);
}
}
return screened;
}


引数の値がクラスであることを保証するclass_()メソッドを追加しました。

上のnew_()メソッドの始めで使われます。


Interpreter.java

    public Class<?> class_(Object value) throws Exception {

if (value instanceof Class<?>) {
return (Class<?>) value;
} else if (value instanceof Variable) {
Variable v = (Variable) value;
return class_(v.value);
}
throw new Exception("right value error");
}

引数が値であることを保証するメソッドの、value()メソッドを変更しました。

いままでの実装方針は、引数valueの型が値として妥当か調べていました。

オブジェクトを生成できるようになったので、値の型を全て調べて妥当か判断することができなくなりました。

そこで値として不適切であることをあらわす値の、void_をスタティックフィールド変数に加えました。

もし引数valuevoid_ならば値として不適切とみなす実装方針へ変更しました。


Interpreter.java


Scope global;
Scope local;
List<Token> body;
// Add
public static Object void_ = new Object();

public Object value(Object value) throws Exception {
if (value == void_) {
throw new Exception("right value error");
} else if (value instanceof Variable) {
Variable v = (Variable) value;
return value(v.value);
}
return value;
}


値として不適切であることをあらわす値のvoid_追加に対応して、

いままで値を返さないものはnullを返していたところをvoid_を返すように変更しました。

下の各メソッドで// Updateとある箇所をvoid_を返すようにしました。


Interpreter.java

    public Object body(List<Token> body, boolean[] ret, boolean[] brk) throws Exception {

for (Token exprs : body) {
if (exprs.kind.equals("if")) {
Object val = if_(exprs, ret, brk);
if (ret != null && ret[0]) {
return val;
}
} else if (exprs.kind.equals("ret")) {
if (ret == null) {
throw new Exception("Can not return");
}
ret[0] = true;
if (exprs.left == null) {
// Update
return void_;
} else {
return expression(exprs.left);
}
} else if (exprs.kind.equals("while")) {
Object val = while_(exprs, ret);
if (ret != null && ret[0]) {
return val;
}
} else if (exprs.kind.equals("brk")) {
if (brk == null) {
throw new Exception("Can not break");
}
brk[0] = true;
// Update
return void_;
} else if (exprs.kind.equals("var")) {
var(exprs);
} else {
expression(exprs);
}
}
// Update
return void_;
}

public Object ret(Token token) throws Exception {
if (token.left == null) {
// Update
return void_;
}
return expression(token.left);
}

public Object if_(Token token, boolean[] ret, boolean[] brk) throws Exception {
List<Token> block;
if (isTrue(token.left)) {
block = token.block;
} else {
block = token.blockOfElse;
}
if (block != null) {
return body(block, ret, brk);
} else {
// Update
return void_;
}
}

public Object while_(Token token, boolean[] ret) throws Exception {
boolean[] brk = new boolean[1];
Object val;
while (isTrue(token.left)) {
val = body(token.block, ret, brk);
if (ret != null && ret[0]) {
return val;
}
if (brk[0]) {
// Update
return void_;
}
}
// Update
return void_;
}

public Object var(Token token) throws Exception {
for (Token item : token.block) {
String name;
Token expr;
if (item.kind.equals("ident")) {
name = item.value;
expr = null;
} else if (item.kind.equals("sign") && item.value.equals("=")) {
name = item.left.value;
expr = item;
} else {
throw new Exception("var error");
}
if (!local.variables.containsKey(name)) {
newVariable(name);
}
if (expr != null) {
expression(expr);
}
}
// Update
return void_;
}

public static class Println extends Func {
public Println() {
name = "println";
}

@Override
public Object invoke(List<Object> args) throws Exception {
Object arg = args.size() > 0 ? args.get(0) : null;
System.out.println(arg);
// Update
return void_;
}
}


以上の実装を使って下のプログラム

var dateClass = loadClass("java.util.Date")

var date = new dateClass()
println(date.toString())

を実行し、Dateオブジェクトを生成して実行日時を出力します。


Interpreter.java

    public static void main(String[] args) throws Exception {

String text = "";
text += "var dateClass = loadClass(\"java.util.Date\")";
text += "var date = new dateClass()";
text += "println(date.toString())";
List<Token> tokens = new Lexer().init(text).tokenize();
List<Token> blk = new Parser().init(tokens).block();
new Interpreter().init(blk).run();
// --> Sat Jun 17 18:29:13 JST 2017 (実行した日時)
}

実装は以上です。

ありがとうございました。


おわりに

ソースの全文はこちらで公開しています。

Calc

https://github.com/quwahara/Calc/tree/article-19-class-loading/Calc/src/main/java

続きの記事があります。

スタティックメソッド呼び出しに対応する

http://qiita.com/quwahara/items/a8ef47f78b1479a117de