Edited at

18 JSON風のオブジェクト定義に対応する

More than 1 year has passed since last update.


はじめに

JSON風のオブジェクト定義に対応したいと思います。

この記事は「配列に対応する」の続きの記事です。


JSON風のオブジェクト定義対応でやりたいこと

JSON風のオブジェクト定義でやりたいことを確認します。

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

1行目でオブジェクトを作成し、変数objへ代入します。

5行目ではオブジェクトの項目数2を出力します。

6行目ではオブジェクトのkey2に対応する値、"value2"が出力されることを目指します。

var obj = {

"key1": "value1",
"key2": "value2",
}
println(obj.size())
println(obj["key2"])

obj.key2 のような . による値のアクセスには対応しません。


実装の仕方

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


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

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


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

構文解析への実装はオブジェクト定義を生成する構文に対応します。

オブジェクトの要素へアクセスする構文は、前回の配列へアクセスする構文への対応でまかなえています。

オブジェクトを生成する構文は、{トークンで始まります。

他の1つ目のトークンで構文で決まるものと同じように、lead()メソッドへ実装します。


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

オブジェクトの実体は、LinkedHashMap<String, Object>を使うことにしました。

構文に対する処理は、オブジェクトの生成とオブジェクト要素へのアクセスを実装します。

オブジェクトの生成では、LinkedHashMap<String, Object>を生成して要素を追加していきます。

オブジェクト要素へのアクセスはLinkedHashMap<String, Object>::get()メソッド呼び出しを行います。


Javaで実装してみる

実装にうつります。

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

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


Lexer.java

Lexer.javaの実装です。

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

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

:の検出を追加しました。


Lexer.java

    private boolean isSymbolStart(char c) {

// Update
return c == ',' || c == ':';
}

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


Parser.java

Parser.javaの実装です。

オブジェクト生成構文の解析を実装します。

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

// Addのところへオブジェクト生成構文解析を行うnewMap()メソッドの呼び出しを追加しました。


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);
} 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);
// Add
} else if (token.kind.equals("curly") && token.value.equals("{")) {
return newMap(token);
} else {
throw new Exception("The token cannot place there.");
}
}

オブジェクト生成構文解析を行うnewMap()メソッドを追加しました。

,で区切られた要素を}トークンに達するまで、{トークンのparamsへ集めます。

要素は":"トークンのleftにキー項目を、rightに値項目を保持します。

":"トークンを{トークンのparamsへ追加します。

トークンの種類kindnewMapにしました。

オブジェクトが[ {"key1": "value1"}, {"key2": "value2"}, ]のように最後の要素が空だったら、

それは要素として無視するようにしました。


Parser.java

    private Token newMap(Token token) throws Exception {

token.kind = "newMap";
token.params = new ArrayList<Token>();
while(true) {
if (token().value.equals("}")) {
consume("}");
break;
}
Token key = expression(0);
Token colon = consume(":");
colon.left = key;
token.params.add(colon);
if (token().value.equals(",")) {
colon.right = blank;
consume(",");
continue;
}
colon.right = expression(0);
if (token().value.equals(",")) {
consume(",");
continue;
} else {
consume("}");
break;
}
}
return token;
}

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


Interpreter.java

Interpreter.javaの実装です。

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

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

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

// Updateの下にあった配列アクセスのためのaccessArray()メソッドを、accessArrayOrMap()メソッドに変えました。

そのメソッドを配列アクセスとオブジェクトアクセスの両方をするようにしたためです。


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("newMap")) {
return newMap(expr);
} else if (expr.kind.equals("newArray")) {
return newArray(expr);
// Update
} 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");
}
}

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

LinkedHashMap<String, Object>を生成して要素を追加したら、それを返します。


Interpreter.java

    public Object newMap(Token expr) throws Exception {

Map<String, Object> m = new LinkedHashMap<>();
for (Token item : expr.params) {
String key = identOrString(item.left);
Object value = value(expression(item.right));
m.put(key, value);
}
return m;
}

引数のトークンがオブジェクトのキーとして妥当かを保証するidentOrString()メソッドを追加しました。

上のnewMap()メソッドで使われます。

引数のトークン自身が識別子であるか、引数のトークンを実行した結果、Stringであれば妥当とします。


Interpreter.java

    public String identOrString(Token expr) throws Exception {

if (expr.kind.equals("ident")) {
return expr.value;
} else {
return string(expression(expr));
}
}

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

最初の// AddのところはMap<String, Object>を値として許容するようにしました。


Interpreter.java

    public Object value(Object value) throws Exception {

if (value instanceof Integer) {
return value;
} else if (value instanceof String) {
return value;
} else if (value instanceof List<?>) {
return value;
// Add
} else if (value instanceof Map<?, ?>) {
return value;
} else if (value == null) {
return value;
} else if (value instanceof Func) {
return value;
} else if (value instanceof Variable) {
Variable v = (Variable) value;
return value(v.value);
}
throw new Exception("right value error");
}

配列アクセスのためのaccessArray()メソッドを、

配列またはオブジェクトアクセスのためのaccessArrayOrMap()メソッドへ変更しました。

引数のexpr[トークンが渡されます。

[トークンのleftList<?>なら配列として扱い、Map<?, ?>ならオブジェクトとして扱います。

[トークンのrightは配列ならインデックスとして、オブジェクトならキーとして扱います。


Interpreter.java

    @SuppressWarnings("unchecked")

public Object accessArrayOrMap(Token expr) throws Exception {
Object v = value(expression(expr.left));
if (v instanceof List<?>) {
List<Object> ar = (List<Object>) v;
Integer index = integer(expression(expr.right));
return ar.get(index);
} else if (v instanceof Map<?, ?>) {
Map<String, Object> map = (Map<String, Object>) v;
String key = string(expression(expr.right));
return map.get(key);
} else {
throw new Exception("accessArrayOrMap error");
}
}

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

var obj = {

"key1": "value1",
"key2": "value2",
}
println(obj.size())
println(obj["key2"])

を実行し、オブジェクトの要素数2key2と関連する値"v2"を出力します。


Interpreter.java

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

String text = "";
text += "var m = {";
text += " key1: \"v1\",";
text += " key2: \"v2\",";
text += "}";
text += "println(m.size())";
text += "println(m[\"key2\"])";
List<Token> tokens = new Lexer().init(text).tokenize();
List<Token> blk = new Parser().init(tokens).block();
new Interpreter().init(blk).run();
// --> 2
// --> v2
}

実装は以上です。

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


おわりに

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

Calc

https://github.com/quwahara/Calc/tree/article-18-map/Calc/src/main/java

続きの記事があります。

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

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