LoginSignup
4
4

More than 5 years have passed since last update.

Scalaでパーサーを作ってみる

Last updated at Posted at 2014-01-27

たまにパーサーを定義して自分用コマンドを作りたくなったり、
整形されたテキストを解析して使いやすいデータにしたいということがあるはずだ
そういう時にはScalaのParserモナドを使うと便利なようだ

試しに簡単な足し算をパースするようなプログラムを書いてみる

TestParser.scala
import scala.util.parsing.combinator._

object TestParser extends RegexParsers {
  // <digit> ::= "0" | ... | "9"
  def digit : Parser[Char] = acceptIf(c => ('0' <= c && c <= '9'))(e => "Err: Digit: "+e)

  // <number> ::= <digit>+
  def number : Parser[Int] = rep1(digit).mkString.toInt

  // <expr> ::= <number> "+" <number>
  def expr : Parser[(Int, Int)] = for {
    a <- number
    _ <- accept('+')
    b <- number
  } yield (a, b)
}

TestParser.parseAll(TestParser.expr, "1+2").map { println _ }  // ==実行結果==> (1, 2)

この例では数字をdigitとして 0 ~ 9 の値を受け取るパーサー、
numberは数字(digit)が1回以上記されているものを受け取るパーサー、
exprはnumberパーサーを使った足し算のパーサーになっている

ここで注目してもらいたいのは、exprは numberと'+'一文字だけを受け取るパーサーの
組み合わせで作られているということだ。exprのfor式は中身のどれか一つでも失敗になると
全体として失敗し、成功すると値を返すということになっており、これ自体もひとつのパーサーとして
見ることができることから、レゴブロックを組み合わせてお城を作るみたいにパーサーを書けてしまう。

これをちょっと拡張してコマンドをパースできるように改造してみると下のようになる。
ちょっと見てもらえると分かると思うが、digit, number, expr パーサーには
一切手を触れずに拡張できるのが分かる。こんなようにScalaに限らずパーサを書くには
モナドを使ったほうが便利で楽しく書けるんじゃないだろうか。

TestParser2.scala
import scala.util.parsing.combinator._

object TestParser2 extends RegexParsers {
  override val skipWhitespace = false

  // <digit> ::= "0" | ... | "9"
  def digit : Parser[Char] = acceptIf(c => ('0' <= c && c <= '9'))(e => "Err: Digit: "+e)

  // <number> ::= <digit>+
  def number : Parser[Int] = for {
    cs <- rep1(digit)
  } yield cs.mkString.toInt

  // <expr> ::= <number> "+" <number>
  def expr : Parser[(Int, Int)] = for {
    a <- number
    _ <- accept('+')
    b <- number
  } yield (a, b)

  // <command> ::= <name> [" "+] [<expr>]
  def command : Parser[String] = for {
    com <- """[a-z]+""".r
    _ <- rep(accept(' '))
    ns <- opt(expr)
  } yield {
    com.mkString match {
      case "add" =>
        ns.map{ case (a, b) => a + b }.getOrElse("").toString
      case "hello" =>
        "world"
      case _ =>
        "Err: Invalid syntax"
    }
  }
}

// OK
TestParser2.parseAll(TestParser2.command, "add 1+2").map{ println _ } // ==> 3
TestParser2.parseAll(TestParser2.command, "add 2+3").map{ println _ } // ==> 5
TestParser2.parseAll(TestParser2.command, "hello").map{ println _ }   // ==> world

// FAIL
TestParser2.parseAll(TestParser2.command, "add 1+a").map{ println _ } // 何も出力されない

追伸:
このコードは以下のように実行することができる

$ scala TestParser.scala
(1, 2)
$ scala TestParser2.scala
3
5
world

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4