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

# はじめに

JSON風のオブジェクト定義に対応したいと思います。
この記事は「[配列に対応する](http://qiita.com/quwahara/items/b4f821a797a146c8d873)」の続きの記事です。

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

JSON風のオブジェクト定義でやりたいことを確認します。
例えば下のようなプログラムがあります。
1行目でオブジェクトを作成し、変数`obj`へ代入します。
5行目ではオブジェクトの項目数`2`を出力します。
6行目ではオブジェクトの`key2`に対応する値、`"value2"`が出力されることを目指します。

```JS
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()`メソッドを変更します。
`:`の検出を追加しました。

```Java:Lexer.java
    private boolean isSymbolStart(char c) {
        // Update
        return c == ',' || c == ':';
    }
```

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

## Parser.java

Parser.javaの実装です。

オブジェクト生成構文の解析を実装します。
1つ目のトークンで構文が決まるものを実装する`lead()`メソッドの
`// Add`のところへオブジェクト生成構文解析を行う`newMap()`メソッドの呼び出しを追加しました。

```Java: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`へ追加します。
トークンの種類`kind`は`newMap`にしました。
オブジェクトが`[ {"key1": "value1"}, {"key2": "value2"}, ]`のように最後の要素が空だったら、
それは要素として無視するようにしました。

```Java: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()`メソッドに変えました。
そのメソッドを配列アクセスとオブジェクトアクセスの両方をするようにしたためです。

```Java: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>`を生成して要素を追加したら、それを返します。

```Java: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`であれば妥当とします。

```Java: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>`を値として許容するようにしました。

```Java: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`は`[`トークンが渡されます。
`[`トークンの`left`が`List<?>`なら配列として扱い、`Map<?, ?>`ならオブジェクトとして扱います。
`[`トークンの`right`は配列ならインデックスとして、オブジェクトならキーとして扱います。

```Java: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");
        }
    }
```

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

```JS
var obj = {
  "key1": "value1",
  "key2": "value2",
}
println(obj.size())
println(obj["key2"])
```

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

```Java: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