Edited at

16 メソッド呼び出しに対応する

More than 1 year has passed since last update.


はじめに

基本的なデータ構造の配列に対応したいと思います。

この記事はで文字列に対応しました。

文字列に対応したら、文字列の部分文字列も扱えるようにしたいものです。

ここでは文字列から部分文字列を取り出せるように、メソッド呼び出しを行えるようにしたいと思います。


メソッド呼び出し対応でやりたいこと

メソッド呼び出し対応でやりたいことを確認します。

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

変数hwへ文字列を代入したら、変数hwのインスタンスメソッドsubstring()を呼び出せるようにすることを目指します。

プログラム最後のprintln()メソッド呼び出しでは、変数hwの部分文字列、Helloが出力されることを目指します。

var hw = "Hello world!"

var h = hw.substring(0, 5)
println(h)

やらないこととしては、スタティックメソッド呼び出しには対応しません。


実装の仕方

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


字句解析(Lexer)の実装の仕方

.を解析する機能がないので追加します。


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

Lexerの機能追加で.トークンが追加になります。

それを解析する機能を追加します。

.トークンは二項演算子のように解析すればよいので、そのように変更していきます。


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

インスタンスメソッドの呼び出しを、既に実装がある関数呼び出しと同等に扱えるように実装していきます。

関数呼び出しでは、関数呼び出しのきっかけになる(トークンの左側のトークンは、

関数名か変数名のトークン1つだけでした。

メソッド呼び出しの(トークンの左側のトークンは、

.トークンとその左右のトークンも、呼び出しに関わります。

そのため.トークンとその左右のトークンを1つの情報として扱えるように、

それらをまとめるクラスを導入します。

まとめたクラスを、関数呼び出しと同等に扱えるようにしていきます。


Javaで実装してみる

実装にうつります。

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

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


Lexer.java

Lexer.javaの実装です。

.の字句解析機能を追加します。

isDotStart()メソッドを追加します。

.を検出します。


Lexer.java

    private boolean isDotStart(char c) {

return c == '.';
}

dot()メソッドを追加します。

.をトークンへ解析します。

.を表すkinddotにします。


Lexer.java

    private Token dot() throws Exception {

Token t = new Token();
t.kind = "dot";
t.value = Character.toString(next());
return t;
}

nextToken()メソッドを変更します。

// Addのところへ、追加したメソッドの呼び出しを追加します。

これで.をトークンへ分解できます。


Lexer.java

    public Token nextToken() throws Exception {

skipSpace();
if (isEOT()) {
return null;
} else if (isSignStart(c())) {
return sign();
// Add
} else if (isDotStart(c())) {
return dot();
} else if (isDigitStart(c())) {
return digit();
} else if (isStringStart(c())) {
return string();
} else if (isIdentStart(c())) {
return ident();
} else if (isParenStart(c())) {
return paren();
} else if (isCurlyStart(c())) {
return curly();
} else if (isSymbolStart(c())) {
return symbol();
} else {
throw new Exception("Not a character for tokens");
}
}

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


Parser.java

Parser.javaの実装です。

トークンの意味に対して、どう動作するかの定義を追加します。

// Update 1のところへ.の順序付けの度合いを追加しました。

.+*のような算術的な演算子よりも、

強く左右のトークンを結びつけるので、

+*の度合いよりも大きい80になっています。

// Update 2のところへ"dot"を追加しました。

.の構文は二項演算子と同等で、

構文解析も二項演算子と同様に行えるので、

二項演算子の構文解析の処理対象になるように、

binaryKinds"dot"を追加しました。


Parser.java

    public Parser() {

degrees = new HashMap<>();
// Update 1
degrees.put(".", 80);
degrees.put("(", 80);
degrees.put("*", 60);
degrees.put("/", 60);
degrees.put("+", 50);
degrees.put("-", 50);
degrees.put("==", 40);
degrees.put("!=", 40);
degrees.put("<", 40);
degrees.put("<=", 40);
degrees.put(">", 40);
degrees.put(">=", 40);
degrees.put("&&", 30);
degrees.put("||", 30);
degrees.put("=", 10);
factorKinds = Arrays.asList(new String[] { "digit", "ident", "string" });
// Update 2
binaryKinds = Arrays.asList(new String[] { "sign", "dot" });
rightAssocs = Arrays.asList(new String[] { "=" });
unaryOperators = Arrays.asList(new String[] { "+", "-", "!" });
reserved = Arrays.asList(new String[] { "function", "return", "if", "else", "while", "break", "var" });
}

構文解析の処理自体は、特に変更や追加を行いません。

定義への追加で既にある二項演算子の構文解析の対象になるからです。

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


Interpreter.java

Interpreter.javaの実装です。

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

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

// Addの下へ.を表すトークンdotのための分岐を追加しました。


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("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);
// Add
} else if (expr.kind.equals("dot")) {
return dot(expr);
} else {
throw new Exception("Expression error");
}
}

dot()メソッドを追加しました。

上で追加された分岐で呼び出されます。

dot()メソッドは、.の左側と右側をペアにしたクラス、

Dottedのインスタンスを返します。

.の左側は、値であることを保証するvalue()メソッドの戻り値を割り付けます。

.の左側が常にメソッド呼び出しのインスタンスとして、保証されるようにするためです。


Interpreter.java

    public Object dot(Token token) throws Exception {

Dotted d = new Dotted();
d.left = value(expression(token.left));
d.right = token.right;
return d;
}

public static class Dotted {
public Object left;
public Token right;
}


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

func()メソッドは引数のvalueが、関数のように呼び出し可能であることを保証します。

上で追加したDottedが引数のvalueで渡ってきたときの処理を// Addの下へ追加しました。

引数がDottedだった場合は、MethodFuncクラスのインスタンスを返すようにしています。

MethodFuncクラスは呼び出し可能な抽象クラスのFuncを継承し、

メソッド呼び出しに必要な情報をフィールド変数に持つようにしています。


Interpreter.java

    public Func func(Object value) throws Exception {

if (value instanceof Func) {
return (Func) value;
// Add
} else if (value instanceof Dotted) {
Dotted d = (Dotted) value;
MethodFunc mf = new MethodFunc();
mf.name = d.right.value;
mf.class_ = d.left.getClass();
mf.target = d.left;
return mf;
} else if (value instanceof Variable) {
Variable v = (Variable) value;
return func(v.value);
} else {
throw new Exception("Not a function");
}
}

MethodFuncクラスを追加しました。

func()メソッドで返すようにしたものです。

このクラスのinvokeメソッドでインスタンスメソッドの呼び出しを行います。

インスタンスメソッド呼び出しを行うには、メソッドを表す情報を得る必要があります。

メソッドの情報はクラスを表す情報から得ることができます。

class_フィールド変数がそのクラスの情報にあたります。

クラス情報からメソッド情報を得ることができますが、

クラスに複数定義されているメソッドのうち、

呼び出したいメソッドの情報を1つ選ぶ必要があります。

invokeメソッドでは、呼び出したいメソッドの情報を1つに選ぶために、

メソッド名やinvokeメソッドの引数の型情報を使って絞り込みます。

該当するメソッド情報を見つけられないか、1つに絞り込めなかったときはエラーにしています。

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

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


Interpreter.java

    public static class MethodFunc extends Func {

// メソッド呼び出し対象の型を表します
public Class<?> class_;
// メソッド呼び出し対象のインスタンスを表します
public Object target;

@Override
public Object invoke(List<Object> args) throws Exception {

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

// メソッド呼び出し対象の型が持つメソッド情報の一覧を、
// このMethodFuncの名前と同じもののみに絞った一覧にします
List<Method> mByName = methodByName(class_.getMethods(), name);

// 名前で絞ったメソッド情報の一覧を、
// 引数の型の一覧が代入可能なシグニチャーになっているもののみに絞った一覧にします
List<Method> mByAssignable = methodByAssignable(mByName, aClasses);

// 絞った結果、該当するメソッド情報がなかったらエラー
if (mByAssignable.size() == 0) {
throw new Exception("MethodFunc.invoke error");
}

Method method;
if (mByAssignable.size() == 1) {

// 絞った結果、該当するメソッド情報が1つだったら、
// それが呼び出し対象のメソッド情報
method = mByAssignable.get(0);

} else {

// 絞った結果、該当するメソッド情報が2つ以上だったら、さらに絞り込みます。
// 代入可能なシグニチャーで絞ったメソッド情報の一覧を、
// 引数の型の一覧が完全に一致するシグニチャーになっているもののみに絞り込みます。
List<Method> mByAbsolute = methodByAbsolute(mByAssignable, aClasses);

// 絞った結果、該当するメソッド情報が1つにならなかったらエラー
if (mByAbsolute.size() != 1) {
throw new Exception("MethodFunc.invoke error");
}

// 絞った結果、該当するメソッド情報が1つだったら、
// それが呼び出し対象のメソッド情報
method = mByAbsolute.get(0);

}

// 1つに絞れたメソッド情報を使って、メソッド呼び出しを行う
Object val = method.invoke(target, 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 List<Method> methodByName(Method[] methods, String name) {
List<Method> ms = new ArrayList<Method>();
for (Method m : methods) {
if (m.getName().equals(name)) {
ms.add(m);
}
}
return ms;
}

public List<Method> methodByAssignable(List<Method> methods, List<Class<?>> aClasses) {
List<Method> candidates = new ArrayList<Method>();

int aSize = aClasses.size();
for (Method m : methods) {
Class<?>[] pTypes = m.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(m);
}
}
return candidates;
}

public List<Method> methodByAbsolute(List<Method> candidates, List<Class<?>> aClasses) {
List<Method> screened = new ArrayList<Method>();
int aSize = aClasses.size();
for (int i = 0; i < aSize; ++i) {
Class<?> ac = aClasses.get(i);
if (ac == null) {
return screened;
}
}
for (Method m : candidates) {
Class<?>[] pTypes = m.getParameterTypes();
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(m);
}
}
return screened;
}
}

public static Class<?> toBoxClass(Class<?> c) {
Class<?> bc;
if (c == boolean.class) {
bc = Boolean.class;
} else if (c == char.class) {
bc = Character.class;
} else if (c == byte.class) {
bc = Byte.class;
} else if (c == short.class) {
bc = Short.class;
} else if (c == int.class) {
bc = Integer.class;
} else if (c == long.class) {
bc = Long.class;
} else if (c == float.class) {
bc = Float.class;
} else if (c == double.class) {
bc = Double.class;
} else {
bc = c;
}
return bc;
}


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

var hw = "Hello world!"

var h = hw.substring(0, 5)
println(h)

を実行し、

プログラム最後のprintln()メソッド呼び出しでHelloを出力します。


Interpreter.java

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

String text = "";
text += "var hw = \"Hello world!\"";
text += "var h = hw.substring(0, 5)";
text += "println(h)";
List<Token> tokens = new Lexer().init(text).tokenize();
List<Token> blk = new Parser().init(tokens).block();
new Interpreter().init(blk).run();
// --> Hello
}

実装は以上です。

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


おわりに

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

Calc

https://github.com/quwahara/Calc/tree/article-16-method-call/Calc/src/main/java

続きの記事があります。

配列に対応する

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