kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

Scalaで作るPackrat Parserコンビネータ

【急募】PEGパーサのメモ化の実装

http://twitter.com/anatoo/status/1253132392

とかあったんで、せっかくなのでサクっと実装してみた。
ポイントは、PEGパーサコンビネータでは、ParserがString => Option[(A, String)]を継承していたのを、Int => Option[(A, Int)]を継承するようにしたことと、Parserの外側のクラスに入力文字列を表すメンバinputを追加したこと。Stringを引数に取る形でも良いのだけど、そうすると、テーブルのルックアップコストが高くなってしまうので、メモ化する意味がほとんど無くなってしまう。

trait PackratParser[+A] {
  val input: String
  abstract class Parser[+A] extends (Int => Option[(A, Int)]) {
    def /[B >: A](that: => Parser[B]): Parser[B] = parserOf{pos =>
      this(pos) orElse that(pos)
    }
    def ~[B](that: => Parser[B]): Parser[(A, B)] = parserOf{pos =>
      for((r1, rpos1) <- this(pos);
          (r2, rpos2) <- that(rpos1)) yield ((r1, r2), rpos2)
    }
    def ^^[B](f: A => B): Parser[B] = parserOf{pos =>
      for((r, rpos) <- this(pos)) yield (f(r), rpos)
    }
    def ? : Parser[Option[A]] = parserOf{pos =>
      this(pos).map{p => (Some(p._1), p._2)}.orElse(Some(None, pos))
    }
    def * : Parser[List[A]] = parserOf{pos =>
      def parse(pos: Int) : (List[A], Int) = {
        this(pos) match {
          case Some((r, rpos)) => val (r2, rpos2) = parse(rpos); (r::r2, rpos2)
          case None => (Nil, pos)
        }
      }
      Some(parse(pos))
    }
    def + : Parser[List[A]] = (this ~ this.*) ^^ {p => p._1::p._2}
    def unary_! : Parser[Unit] = parserOf{pos =>
      this(pos) match {
        case Some(_) => None
        case None => Some((), pos)
      }
    }
  }
  class Memoized[+A](val parser: Parser[A]) extends Parser[A] {
    import scala.collection.mutable._
    private[this] val cache = new HashMap[Int, Option[(A, Int)]]
    def apply(pos: Int): Option[(A, Int)] = {
      cache.get(pos).getOrElse {
        val result = parser(pos)
        cache(pos) = result
        result
      }
    }
  }
  def parserOf[A](f: Int => Option[(A, Int)]): Parser[A] = 
    new Parser[A] {
      def apply(param: Int): Option[(A, Int)] = f(param)
    }
  def string(param: String): Parser[String] = parserOf{pos =>
    val in = input.substring(pos)
    if(in startsWith param) Some(param, pos + param.length)
    else None
  }
  def and[A](p: => Parser[A]): Parser[Unit] = !(!p)
  lazy val any: Parser[String] = parserOf{pos =>
	  val in = input.substring(pos)
    if(in.length > 0) Some(in substring(0, 1), pos + 1) else None
  }
  implicit def stringToParser(param: String) = string(param)
  implicit def parserToMemoized[A](p: Parser[A]) = new Memoized(p)
  def parse: Option[(A, String)] = {
    for((r, pos) <- S(0)) yield (r, input.substring(pos))
  }
  val S: Parser[A]
}

使い方は以下のような感じ。

class ParenParser(override val input: String) extends PackratParser[Any] {
  lazy val S: Parser[Any] = A ~ !any
  lazy val A: Parser[Any] = P ~ "+" ~ A / P ~ "-" ~ A / P
  lazy val P: Parser[Any] = "(" ~ A ~ ")" / "1"
}

//memoized
class MemoizedParenParser(override val input: String) extends PackratParser[Any] {
  lazy val S: Memoized[Any] = A ~ !any
  lazy val A: Memoized[Any] = P ~ "+" ~ A / P ~ "-" ~ A / P
  lazy val P: Memoized[Any] = "(" ~ A ~ ")" / "1"
}

def time(any: => Any): Long = {
  val start = System.currentTimeMillis
  any
  System.currentTimeMillis - start
}

val input = "(((((((((((((1)))))))))))))"
printf("not memoized: %dms%n", time{new ParenParser(input).parse})
printf("memoized: %dms%n", time{new MemoizedParenParser(input).parse})

ここでは、メモ化した場合としない場合で著しい差が出る文法に対してマッチングを行い、実行時間を出力しようとしている。このプログラムを実行すると(Intel Core2 Duo 2.40GHz, RAM 2.0GB, JDK1.6.0 Client VM)、

not memoized: 5063ms
memoized: 0ms

となり、メモ化が確かに効いていることが確認できる(ほんとはこんなテキトーな測定方法じゃ良くないけど、明らかに差があるのはわかる)。