scalaでパーザーコンビネーターを書いてみた

パーザーコンビネータとは

パーザを部品として、より複雑なパーザを構築する手法。例えば文字列の一致など基本的なパーサーを作っていおいて、それを組み合わせることで数式のパーサーを作ったりする。

なぜパーザーコンビネータ

基本的な文法を覚えた後の次のステップにちょうど良い規模(小さすぎず、大きすぎず) 言語の機能を幅広く使うことができる

Javaでのパーザコンビネータの実装としてこの記事が参考になりましたので、これをもとに自分なりにScalaで書いてみたいと思います。

基本的なパーザの実装

まずパース結果を表せるようにします。ParseOkはパース成功時に使うクラスになり、valueはパースして得られ値で型はパラメータで渡しています。nextはパースに成功したテキスト以降の文字列になります。それからParseNgがパース失敗時に使うクラスでmessageはパース失敗の内容を保持させるのに使います。

trait ParseResult[T] {
}

case class ParseOk[T](value: T, next: String) extends ParseResult[T] {
  override def toString(): String = "Success(" + value + ", " + next + ")"
}

case class ParseNg[T](message: String, next: String) extends ParseResult[T] {
  override def toString(): String = "Failure(" + message + ", " + next + ")"
}

文字列判定のパーサー

文字列をパースするクラスは以下のようになります。単純にクラス初期化時に渡した文字列と一致した場合はParseOkでそれ以外はParseNgを返すようにしています。

class StringParser(literal: String) extends Parser[String] {

  override def parse(input: String): ParseResult[String] = {
    input.startsWith(literal) match {
      case true => ParseOk(this.literal, input.substring(literal.length))
      case false => ParseNg("expect: " + this.literal, input)
    }
  }
}

例えばhelloという文字列に一致するかどうかを判定するパーサーのインスタンスを生成する場合は以下のようにParser.string("hello")とできるようにしておきます。

trait Parser[T] {
  def parse(input: String): ParseResult[T] = ???

  def string(literal: String):Parser[String] = new StringParser(literal)
}

object Parser extends Parser[Any] {}

このパーサーのテストコードは以下のようになります。

class StringParserTest extends FlatSpec {

  "parse" should "successt" in {
    val parse = Parser.string("Hello")
    val result1 = parse.parse("Hello, world")

    assert(result1.isInstanceOf[ParseOk[_]])
    assert(result1.asInstanceOf[ParseOk[_]].next.equals(", world"))
  }

  "parse" should "failer" in {
    val parse = Parser.string("Hello")
    val result1 = parse.parse("hello, world")

    assert(result1.isInstanceOf[ParseNg[_]])
  }
}

0回以上の繰り返しを判定するパーサー

次に0回以上の繰り返しを判定するパーサーになります。0回以上のためインスタンス生成時の引数として任意のパーサーを受け取るようにしているのでパース結果の値として型パラメータをうけとれるようにしています。

class ManyParser[T](parser: Parser[T]) extends Parser[List[T]] {

  override def parse(input: String): ParseResult[List[T]] = {
    var next = input
    var values = List.empty[T]
    var isContinue = true
    while(isContinue) {
      val result = parser.parse(next)
      if(result.isInstanceOf[ParseOk[_]]) {
        values = values :+ result.asInstanceOf[ParseOk[T]].value
        next = result.asInstanceOf[ParseOk[T]].next
      } else {
        isContinue = false
      }
    }
    ParseOk(values, next)
  }
}

文字列判定のパーサーと同様にインスタンス生成を生成できるようにParserトレイトを以下のように修正します。

trait Parser[T] {
  def parse(input: String): ParseResult[T] = ???

  def many(): Parser[List[T]] = new ManyParser[T](this)
  def string(literal: String):Parser[String] = new StringParser(literal)
}

object Parser extends Parser[Any] {}

1回以上の繰り返しを判定するパーサー

1回以上の繰り返しの場合はパース成功結果を保持するりすとのlengthが0の場合は失敗で、1以上の場合は成功にしています。

class PlusParser[T](parser: Parser[T]) extends Parser[List[T]] {

  override def parse(input: String): ParseResult[List[T]] = {
    var next = input
    var values = List.empty[T]
    var isContinue = true
    var result: ParseResult[T] = null
    while(isContinue) {
      result = parser.parse(next)
      if(result.isInstanceOf[ParseOk[_]]) {
        values = values :+ result.asInstanceOf[ParseOk[T]].value
        next = result.asInstanceOf[ParseOk[T]].next
      } else {
        isContinue = false
      }
    }
    values.length match {
      case 0 => ParseNg(result.asInstanceOf[ParseNg[_]].message, input)
      case _ => ParseOk(values, next)
    }
  }
}

オプショナルなパースを行う

省略可能な文字列の判定などに使うためのオプショナルなパーサーですが、インスタンス生成時の引数として受け取ったパーサーでパースを行いその結果に限らずパース成功として結果を返すようにします。オプショナルなパーサーのため型パラメータとして受け取った型をOptionalでくるんで返すようにしており、パース成功時はSome(result.value)で失敗時はNoneをParseOkにセットしています。

class OptionalParser[T](parser: Parser[T]) extends Parser[Option[T]] {

  override def parse(input: String): ParseResult[Option[T]] = {
    parser.parse(input) match {
      case result: ParseOk[T] => ParseOk(Some(result.value), result.next)
      case result: ParseNg[_] => ParseOk(None, input)
    }
  }
}

2つのパーサーを候補として片方で成功したら成功とするパーサー

インスタンス生成時に受け取った2つのパーサーでor条件とするパーサーです。インスタンス生成時の引数として受け取るがたパラメータは別々で分けれた方が良いと思うのですが、パース結果の型をうまいこと分ける方法が思いつかなかったので同じものにしています。

class OrParser[T](lps: Parser[T], rps: Parser[T]) extends Parser[T] {

  override def parse(input: String): ParseResult[T] = {
    lps.parse(input) match {
      case lResult: ParseOk[T] => ParseOk(lResult.value, lResult.next)
      case lResult: ParseNg[_] => {
        rps.parse(input) match {
          case rResult: ParseOk[T] => ParseOk(rResult.value, rResult.next)
          case rResult: ParseNg[_] => ParseNg(rResult.message, rResult.next)
        }
      }
    }
  }
}

2つのパーサーの繰り返し

2つのパーサーをインスタンスの引数とし、パース結果としてインスタンス引数で受け取った2つのパーサーの結果をタプルにして返すようにします。

class Pair2[T,U](lps: Parser[T], rps: Parser[U]) extends Parser[(T,U)] {
  override def parse(input: String): ParseResult[(T, U)] = {
    lps.parse(input) match {
      case lResult: ParseOk[T] => {
        rps.parse(lResult.next) match {
          case rResult: ParseOk[U] => ParseOk((lResult.value, rResult.value), rResult.next)
          case rResult: ParseNg[_] => ParseNg(rResult.message, rResult.next)
        }
      }
      case lResult: ParseNg[_] => ParseNg(lResult.message, lResult.next)
    }
  }
}

3つのパーサーの繰り返し

今度は3つのパーサーをインスタンス引数として同様に処理します。2つのパーサーの繰り返しを組み合わせても使えるのですが、パース結果を扱いやすくするために用意しています。

class Pair3[T,U,V](lps: Parser[T], cps: Parser[U], rps: Parser[V]) extends Parser[(T,U,V)] {
  override def parse(input: String): ParseResult[(T,U,V)] = {
    lps.parse(input) match {
      case lResult: ParseOk[T] => {
        cps.parse(lResult.next) match {
          case cResult: ParseOk[U] => {
            rps.parse(cResult.next) match {
              case rResult: ParseOk[V] => ParseOk((lResult.value, cResult.value, rResult.value), rResult.next)
              case rResult: ParseNg[_] => ParseNg(rResult.message, rResult.next)
            }
          }
          case cResult: ParseNg[_] => ParseNg(cResult.message, cResult.next)
        }
      }
      case lResult: ParseNg[_] => ParseNg(lResult.message, lResult.next)
    }
  }
}

終端判定のパーサー

終端の判定も必要になるので用意しておきます。今回はクラスとして定義していますがインスタンス生成時の引数もないのでobjectとして定義しておいてもよさそうです。

class EOFParser() extends Parser[String] {

  override def parse(input: String): ParseResult[String] = {
    input.length == 0 match {
      case true => ParseOk("", "")
      case false => ParseNg("expect EOF:, actual " + input, input)
    }
  }
}

ストップッワードによる文字列判定

ダブルクォーテーションで囲まれた文字列を判定するときなどに使えるようにストップワードによる文字列判定を行えるようにしたいと思います。ただストップワードをダブルクォーテーションにした場合であっても文字列内にダブルクォーテーションを使いたいことがあるのでエスケープできるように引数として受け取ります。

class StopWithEscape(stop: String, escapes: Map[String, String]) extends Parser[String] {
  assert(stop.length > 0)

  override def parse(input: String): ParseResult[String] = {
    var next = input
    var continueFlg = true
    var ret = ""
    while(continueFlg) {
      escapes.foreach{case (key, value) => {
        if(next.startsWith(key)) {
          ret += value
          next = next.substring(key.length)
        }
      }}
      if(next.length > 0 && !next.startsWith(stop)) {
        ret += next(0)
        next = next.substring(1)
      } else {
        continueFlg = false
      }
    }
    ParseOk(ret, next)
  }
}

基本のパーサーとしてこれだけ準備しておきます。インスタンス生成用のParserトレイトを以下のようにしておきます。

trait Parser[T] {
  def parse(input: String): ParseResult[T] = ???

  def many(): Parser[List[T]] = new ManyParser[T](this)
  def plus(): Parser[List[T]] = new PlusParser[T](this)
  def pair2[U](rps: Parser[U]): Parser[(T,U)] = new Pair2(this, rps)
  def pair3[U,V](cps: Parser[U], rps: Parser[V]): Parser[(T,U,V)] = new Pair3(this, cps, rps)
  def or(p: Parser[T]): Parser[T] = new OrParser(this, p)
  def option(): Parser[Option[T]] = new OptionalParser[T](this)
  def eof:Parser[String] = new EOFParser()
  def string(literal: String):Parser[String] = new StringParser(literal)
  def stop(stop: String, escapes: Map[String, String]):Parser[String] = new StopWithEscape(stop, escapes)

}

object Parser extends Parser[Any] {}

Jsonをパースできるようにする

基本のパーサーは実装できたので今度はそれを組み合わせて使ってみたいと思います。 JSONのパーサーは構文定義をみると練習の規模として丁度よさそうです。

まずパース結果の値としてstring, number, object, array, bool, nullとあるのでそれらを表せるためのクラスを用意します。この時classのequalsメソッドはoverrideしておきます。

trait JValue

case class JString(value: String) extends JValue {

  override def equals(obj: Any): Boolean = {
    obj.isInstanceOf[JString] && value.equals(obj.asInstanceOf[JString].value)
  }
}

case class JNumber(value: Int, decimal: Int, base: Int) extends JValue {

  override def equals(obj: Any): Boolean = {
    obj match {
      case obj: JNumber => value == obj.value && decimal == obj.decimal && base == obj.base
      case _ => false
    }
  }
}

case class JObject(map: Map[JString, JValue]) extends JValue {
  def getMap = map

  override def equals(obj: Any): Boolean = {
    obj match {
      case obj: JObject => {
        (map.size == obj.getMap.size) && map.find(kv => !kv._2.equals(obj.getMap.get(kv._1).get)).isEmpty
      }
      case _ => false
    }
  }
}

case class JArray(value: List[JValue]) extends JValue {
  def getVal = value

  override def equals(obj: Any): Boolean = {
    obj match {
      case obj: JArray => value.length == obj.getVal.length &&
        value.zipWithIndex.find{case (v, index) => !obj.getVal(index).equals(v)}.isEmpty
      case _ => false
    }
  }
}

case class JBool(value: Boolean) extends JValue {
  override def equals(obj: Any): Boolean = obj.isInstanceOf[JBool] && value.equals(obj.asInstanceOf[JBool].value)
}


case class JNull() extends JValue {

  override def equals(obj: Any): Boolean = obj.isInstanceOf[JNull]
}

それからJsonパーサーとしてParserを拡張したJParserを定義します。

trait JParser[T <: JValue] extends Parser[T] {
}

object JParser extends JParser[JValue]

JSONの文字列判定

JSONの文字列判定ですがダブルクォーテーションに囲まれたものになるのでストップワードのパーサーを使って以下のように定義します。

object JStringParser extends JParser[JString] {

  override def parse(input: String): ParseResult[JString] = {
    val parser = new Pair3(Parser.string("\""), Parser.stop("\"", Map("\\\"" -> "\"")), Parser.string("\""))
    parser.parse(input) match {
      case result: ParseOk[(String, String, String)] => ParseOk(JString(result.value._2), result.next)
      case result: ParseNg[_] => ParseNg(result.message, result.next)
    }
  }
}

JSONの数値判定

数値の判定ですが少数や指数を指定できるようにすると以下のように結構ボリュームのあるパーサーになりました。何をやっているのかわかりづらく、対象の構文定義と照らし合わせていかないと読みづらいかと思います。 先に構文定義

number
    integer fraction exponent

integer
    digit
    onenine digits
    '-' digit
    '-' onenine digits

digits
    digit
    digit digits

digit
    '0'
    onenine

onenine
    '1' . '9'

fraction
    ""
    '.' digits

exponent
    ""
    'E' sign digits
    'e' sign digits

sign
    ""
    '+'
    '-'

それから、コードは以下のようになりました。

object JNumberParser extends JParser[JNumber] {
  lazy val signP = Parser.string("+").or(Parser.string("-")).option()
  lazy val baseP = Parser.string("E").or(Parser.string("e"))
  lazy val ztonP = Parser.string("0").or(Parser.string("1")).or(Parser.string("2")).or(Parser.string("3")).
    or(Parser.string("4")).or(Parser.string("5")).or(Parser.string("6")).
    or(Parser.string("7")).or(Parser.string("8")).or(Parser.string("9"))
  lazy val parser1 = new Pair2(new Pair2(signP, ztonP.plus()), (new Pair2(Parser.string("."), ztonP.plus())).option())
  lazy val parser2 = (new Pair3(baseP, signP, ztonP.plus())).option()

  override def parse(input: String): ParseResult[JNumber] = {
    parser1.parse(input) match {
      case result1: ParseOk[((Option[String], List[String]), Option[(String, List[String])])] => {
        parser2.parse(result1.next) match {
          case result2: ParseOk[Option[(String, Option[String], List[String])]] => {
            var value = 0
            var deci = 0
            var base = 0
            val valueStr = result1.value._1._2.foldLeft(result1.value._1._1.getOrElse(""))((acc, cur) => {
              acc + cur
            })
            value = Integer.valueOf(valueStr)
            if(result1.value._2.isDefined) {
              val decStr = result1.value._2.get._2.foldLeft("")((acc, cur) => {
                acc + cur
              })
              deci = Integer.valueOf(decStr)
            }
            if(result2.value.isDefined) {
              val baseStr = result2.value.get._3.foldLeft(result2.value.get._2.getOrElse(""))((acc, cur) => {
                acc + cur
              })
              base = Integer.valueOf(baseStr)
            }
            ParseOk(JNumber(value, deci, base), result2.next)
          }
          case result2: ParseNg[_] => ParseNg(result2.message, result2.next)
        }
      }
      case result1: ParseNg[_] => ParseNg(result1.message, result1.next)
    }
  }
}

この時のテストコード配下になります。

class JNumberParserTest extends FlatSpec{

  "parse" should "successt" in {
    val parser = JNumberParser

    val result1 = parser.parse("123.456e-234")
    assert(result1.isInstanceOf[ParseOk[_]])
    assert(result1.asInstanceOf[ParseOk[JNumber]].value.equals(JNumber(123,456, -234)))

    val result2 = parser.parse("0.5")
    assert(result2.isInstanceOf[ParseOk[_]])
    assert(result2.asInstanceOf[ParseOk[JNumber]].value.equals(JNumber(0,5, 0)))

    val result3 = parser.parse("123.+456e-234")
    assert(result3.isInstanceOf[ParseOk[_]])
    assert(result3.asInstanceOf[ParseOk[JNumber]].value.equals(JNumber(123,0, 0)))

  }
}

JSONのbool判定

true, falseのどちらか判定できれば良いので以下のようになります。

object JBoolParser extends JParser[JBool] {

  override def parse(input: String): ParseResult[JBool] = {
    val parser = Parser.string("true").or(Parser.string("false"))
    parser.parse(input) match {
      case result: ParseOk[String] => ParseOk(JBool(result.value.equals("true")), result.next)
      case result: ParseNg[_] => ParseNg(result.message, result.next)
    }
  }
}

JSONのnull判定

null判定は以下になります。

object JNullParser extends JParser[JNull] {

  override def parse(input: String): ParseResult[JNull] = {
    val parser = Parser.string("null")
    parser.parse(input) match {
      case result: ParseOk[String] => ParseOk(JNull(), result.next)
      case result: ParseNg[_] => ParseNg(result.message, result.next)
    }
  }
}

JSONvalue判定

JSONの構文定義を見てみるとobject, array, string, number, "true", "false", "null"がvalueとなっています。object, arrayはまだ未定義なのですが先にvalueのパーサーを実装しておきます。 まずJSONのパーサーから複数選べるようにできなければいけないのでJSON用のOrパーサーを準備します。基本パーサーのOrパーサーが使えればそちらにしようかと思ったのですがJParserを拡張した形にしたかったので分けています。

class JOrParser[T <: JValue,U <: JValue](parser1: JParser[T], parser2: JParser[U]) extends JParser[JValue] {

  override def parse(input: String): ParseResult[JValue] = {
    parser1.parse(input) match {
      case result1: ParseOk[T] => ParseOk(result1.value, result1.next)
      case result1: ParseNg[_] => {
        parser2.parse(input) match {
          case result2: ParseOk[U] => ParseOk(result2.value, result2.next)
          case result2: ParseNg[_] => ParseNg(result2.message, input)
        }
      }
    }
  }
}

インスタンス生成用のトレイトを以下のように修正します。

trait JParser[T <: JValue] extends Parser[T] {
  def jor[T <: JValue](parser: JParser[T]): JParser[JValue] = new JOrParser(this, parser)
}

object JParser extends JParser[JValue]

それからJSONvalueを判定するパーサーです。

object JValueParser extends JParser[JValue] {
  lazy val parser = JNullParser.jor(JBoolParser).jor(JStringParser).jor(JNumberParser).jor(JArrayParser).jor(JObjectParser)
  override def parse(input: String): ParseResult[JValue] = parser.parse(input)
}

JSONのobject判定

次にobject判定のパーサーです。jsonのデータ構造では入れ子にすることができるのでobjectのkeyとvalueのうちvalueの方をパースするのに先ほど定義したJValueParserを使います。ただJValueParser内でもJObjectParserを使っていて循環参照になっているのですが、scalaではうまいこと初期化することができてエラーにはならないようです。

object JObjectParser extends JParser[JObject] {
  lazy val sp = Parser.string(" ").many()
  lazy val leftP = new Pair3(sp, Parser.string("{"), sp)
  lazy val rightP = new Pair3(sp, Parser.string("}"), sp)
  lazy val kvP = new Pair3(new Pair3(sp, JStringParser, sp), Parser.string(":"), new Pair3(sp, JValueParser, sp))
  lazy val kvsP = new Pair2(Parser.string(","), kvP)
  lazy val parser = (new Pair2(kvP, kvsP.many())).option()

  override def parse(input: String): ParseResult[JObject] = {
    leftP.parse(input) match {
      case leftResult: ParseOk[(List[String], String, List[String])] => {
        parser.parse(leftResult.next) match {
          case result: ParseOk[Option[(((List[String], JString, List[String]), String, (List[String], JValue, List[String])),
            List[((String, ((List[String], JString, List[String]), String, (List[String], JValue, List[String]))))])]] => {
            rightP.parse(result.next) match {
              case rightResult: ParseOk[(List[String], String, List[String])] => {
                var map: Map[JString, JValue] = Map()
                if(result.value.isDefined) {
                  map += result.value.get._1._1._2 -> result.value.get._1._3._2
                  result.value.get._2.foreach(a => {
                    map += a._2._1._2 -> a._2._3._2
                  })
                }
                ParseOk(JObject(map), rightResult.next)
              }
              case rightResult: ParseNg[_] => ParseNg(rightResult.message, rightResult.next)
            }
          }
          case result: ParseNg[_] => ParseNg(result.message, result.next)
        }
      }
      case leftResult: ParseNg[_] => ParseNg(leftResult.message, leftResult.next)
    }
  }
}

JSONの配列判定

次に配列の判定です。ここでもobjectのパーサーと同様にJValueParserと循環参照的になっていますがうまいこと初期化できているようです。

object JArrayParser extends JParser[JArray] {
  lazy val sp = Parser.string(" ").many()
  lazy val leftP = new Pair3(sp, Parser.string("["), sp)
  lazy val rightP = new Pair3(sp, Parser.string("]"), sp)
  lazy val parser = (new Pair2(new Pair3(sp, JValueParser, sp), (new Pair2(Parser.string(","), (new Pair3(sp, JValueParser, sp)))).many())).option()


  override def parse(input: String): ParseResult[JArray] = {
    leftP.parse(input) match {
      case leftResult: ParseOk[(List[String], String, List[String])] => {
        parser.parse(leftResult.next) match {
          case result: ParseOk[Option[((List[String], JValue, List[String]), List[(String, (List[String], JValue, List[String]))])]] => {
            rightP.parse(result.next) match {
              case rightResult: ParseOk[(List[String], String, List[String])] => {
                var value = List.empty[JValue]
                if(result.value.isDefined) {
                  value = value :+ result.value.get._1._2
                  result.value.get._2.foreach(a => {
                    value = value :+ a._2._2
                  })
                }
                ParseOk(JArray(value), rightResult.next)
              }
              case rightResult: ParseNg[_] => ParseNg(rightResult.message, rightResult.next)
            }
          }
          case result: ParseNg[_] => ParseNg(result.message, result.next)
        }
      }
      case leftResult: ParseNg[_] => ParseNg(leftResult.message, leftResult.next)
    }
  }
}

JSON判定のパーサー

最後にJSONを判定するためのパーサーです。

object JsonParser extends JParser[JValue]{
  lazy val parser = new Pair2(JObjectParser.jor(JArrayParser), Parser.eof)

  override def parse(input: String): ParseResult[JValue] = {
    parser.parse(input) match {
      case result: ParseOk[(JValue, String)] => ParseOk(result.value._1, result.next)
      case result: ParseNg[_] => ParseNg(result.message, result.next)
    }
  }
}

テストコードを実行しても大丈夫だったのでJSONはパースできてそうです。

class JsonParserTest extends FlatSpec {

  "parse" should "successt" in {
    val parser = JsonParser

    val result1 = parser.parse(" {\"a\": [ [1, 2, 3,4], {} ]} ")
    assert(result1.isInstanceOf[ParseOk[JValue]])
    assert(result1.asInstanceOf[ParseOk[JValue]].value.equals(JObject(
      Map(JString("a") -> JArray(List(JArray(List(JNumber(1, 0, 0), JNumber(2, 0, 0), JNumber(3, 0, 0), JNumber(4, 0, 0))), JObject(Map.empty[JString, JValue]))))
    )))


    val result2 = parser.parse(" [ [1, 2, 3,4], {} ] ")
    assert(result2.isInstanceOf[ParseOk[JValue]])
    assert(result2.asInstanceOf[ParseOk[JValue]].value.equals(JArray(
      List(JArray(List(JNumber(1, 0, 0), JNumber(2, 0, 0), JNumber(3, 0, 0), JNumber(4, 0, 0))), JObject(Map.empty[JString, JValue]))
    )))
  }

  "parse" should "fail" in {
    val parser = JsonParser

    val result1 = parser.parse(" {\"a\": [ [1, 2, 3,4], {} ]}, {}")
    assert(result1.isInstanceOf[ParseNg[_]])
  }
}

今回使ったソースは以下にあげております。
https://github.com/teruuuuuu/parsers/tree/master/scala/parsers