はじめに
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()
メソッドを変更します。
:
の検出を追加しました。
private boolean isSymbolStart(char c) {
// Update
return c == ',' || c == ':';
}
Lexer.javaの変更は以上です。
Parser.java
Parser.javaの実装です。
オブジェクト生成構文の解析を実装します。
1つ目のトークンで構文が決まるものを実装するlead()
メソッドの
// Add
のところへオブジェクト生成構文解析を行うnewMap()
メソッドの呼び出しを追加しました。
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
へ追加します。
トークンの種類kind
はnewMap
にしました。
オブジェクトが[ {"key1": "value1"}, {"key2": "value2"}, ]
のように最後の要素が空だったら、
それは要素として無視するようにしました。
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()
メソッドに変えました。
そのメソッドを配列アクセスとオブジェクトアクセスの両方をするようにしたためです。
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>
を生成して要素を追加したら、それを返します。
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
であれば妥当とします。
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>
を値として許容するようにしました。
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
は[
トークンが渡されます。
[
トークンのleft
がList<?>
なら配列として扱い、Map<?, ?>
ならオブジェクトとして扱います。
[
トークンのright
は配列ならインデックスとして、オブジェクトならキーとして扱います。
@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"])
を実行し、オブジェクトの要素数2
とkey2
と関連する値"v2"
を出力します。
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