· 5分で読了

ゼロから作る JSON パーサー(2)

この記事は中国語から自動翻訳されたものです。翻訳によりニュアンスが失われている場合があります。

part1 では JSON パーサーの書き方に触れ、文字列をパースする機能を実装した。今回は残りの関数を補完していく。(実際のところ、基本原理さえ理解していれば、残りの関数実装はマニュアル通りに進めるだけだ)

Number

json-grammer

Number の実装も難しくはない。見落としがちなのは小数点、負の数、浮動小数点数の部分、そして指数表記(1e6)だ。(そういえば大文字の E も使えることに先ほど気づいた)

function number(parser) {
  let str = "";
  if (parser.current() === "-") {
    str += "-";
    parser.index += 1;
  }

  let curr = "";
  while (((curr = parser.current()), curr >= "0" && curr <= "9")) {
    str += curr;
    parser.index += 1;
  }

  let isFloat = false;
  // float number
  if (parser.next(".")) {
    str += ".";
    isFloat = true;
    while (((curr = parser.current()), curr >= "0" && curr <= "9")) {
      str += curr;
      parser.index += 1;
    }
  }

  // exponential expression
  let expo = "";
  if (parser.next("e")) {
    curr = "";
    if (parser.next("-")) {
      expo += "-";
    }

    while (((curr = parser.current()), curr >= "0" && curr <= "9")) {
      expo += curr;
      parser.index += 1;
    }
  }

  if (expo) {
    return isFloat
      ? parseFloat(str, 10) * Math.pow(10, +expo)
      : parseInt(str, 10) * Math.pow(10, +expo);
  }

  return isFloat ? parseFloat(str, 10) : parseInt(str, 10);
}
  • 第 1 ステップ:まず負の数かどうかを確認する

  • 第 2 ステップ:while ループを回して数字部分を取得する

  • 第 3 ステップ:小数点の有無を判定する

    • 小数点があれば、再度数字を読み取る
  • 第 4 ステップ:指数表記(大文字または小文字の e)の有無を判定する

    • 指数表記があれば、同様に再度数字を読み取る
  • 第 5 ステップ:文字列を数値に変換する(parseInt または parseFloat を使用)

キーワード(true, false, null)

function keyword(parser) {
  if (parser.next("true")) {
    return true;
  } else if (parser.next("false")) {
    return false;
  } else if (parser.next("null")) {
    return null;
  }
}

この部分は非常にシンプルで、値がマッチしているかどうかを確認するだけだ。

配列(Array)

json-grammer

function array(parser) {
  const arr = [];

  if (parser.current() === "[") {
    parser.next("[");
    parser.skip();

    if (parser.next("]")) {
      return arr;
    }
    let i = 0;
    while (parser.current()) {
      const val = value(parser);
      arr.push(val);

      parser.skip();

      if (parser.current() === "]") {
        parser.next("]");
        return arr;
      }
      parser.next(",");
      parser.skip();
    }
  }

  return arr;
}
  • 第 1 ステップ:まず [ で始まっているかを確認する

    • 続いて ] に遭遇した場合は、空の配列であることを意味する
  • 第 2 ステップ:while ループを回して value 関数を実行し、配列に追加していく

  • ] に遭遇したら配列の終端を意味するので、配列を返す

  • カンマに遭遇したら次の要素があることを意味するので、処理を継続する

これでほぼ完成だが、Repository のコード実装を見てみると、テストの中で specical-character が 1 つ失敗していることがわかる。文字列にはエスケープ文字が含まれる可能性があるためだ。これを実装してみよう。

const escape = {
  '"': '"',
  t: "\t",
  r: "\r",
  "\\": "\\",
};

while (((curr = parser.current()), curr)) {
    if (parser.next('"')) {
      return str;
    } else if (curr === "\\") {
      parser.index += 1;
      const escapeCh = parser.current();
      if (escape[escapeCh]) {
        str += escape[escapeCh];
      }
    } else {
      str += curr;
    }
    parser.index += 1;
  }

僕たちはエスケープ文字の対応表を作成し、テーブルを参照して対応する文字に置換した(ここでは \t と \r のみ実装している)。これで基本的な JSON のテストはパスしたことになる🍻。ただし、エスケープ文字は上記以外にも、Unicode を表す \u を実装する必要があり、この機能はかなり重要だ。

カスタム機能:templatetemplate

パーサーを自作している以上、当然ながら独自の新しい構文を追加することもできる!例えばテンプレート機能を実装したいとして、$$ で囲まれた任意の変数を、渡されたオブジェクトの値で置き換えるとしよう:

{
  "name": $name$
}

次のようになる:

new Parser(string, { name: 'kalan' }).parse();
// { name: "kalan" }

実装

function template(parser) {
  parser.skip();
  if (parser.next("$")) {
    parser.skip();

    if (parser.next("$")) {
      throw new Error("template can not be empty");
    }
    let curr = "";
    let key = "";
    while (((curr = parser.current()), curr)) {
      if (parser.next("$")) {
        return parser.variables[key];
      }
      key += curr;
      parser.index += 1;
    }
  }
}
  • まず $ にマッチさせる
  • 次の $ が現れるまで内容を読み進める
  • $ に遭遇したら while ループを終了し、テンプレート変数を渡された値で置き換えて結果を返す

詳細な実装は template ブランチで確認できる。テスト結果もあわせて見てみてほしい(test/template ディレクトリ内にある)。

おわりに

パーサーを自作することで、複雑な実装をより表現しやすい構文で表すことができる。さらには既存の文法(今回の JSON など)を拡張して、自分が欲しい機能を追加することも可能だ。実用的とは言えないかもしれないが、パースによってどんなことができるのかを示すのがここでの目的だ。

パース自体は非常に重要で面白いものだが、言語をパースすること自体は最初の一歩に過ぎない。JSX を単に JavaScript コードに変換したとしても React のサポートがなければ意味がないし、SQL を抽象構文木に変換してもデータベースの実装がなければ宝の持ち腐れだ。言語をパースする目的は、その後の処理(クエリの実行、DOM へのレンダリングなど)を円滑に行うことにある。

実際、現在ではパーサーの実装をまるごとスキップできるライブラリが数多く存在する。例えば有名な Bison や PEG.js などは、BNF 風の構文を記述するだけで安定したパーサーを自動生成してくれる。自作する時間を省き、言語そのものの実装に集中できるわけだ。

僕たちの今回の JSON パーサーでは、抽象構文木に変換してから最終結果を生成するという手順は踏んでいない。そこで次の段階では、シンプルな HTML をパースし、構文木に変換した上で JavaScript の DOM API を使ってレンダリングすることに挑戦してみようと思う。

関連記事

他のトピックを探索