たまにパーサーを定義して自分用コマンドを作りたくなったり、
整形されたテキストを解析して使いやすいデータにしたいということがあるはずだ
そういう時にはScalaのParserモナドを使うと便利なようだ
試しに簡単な足し算をパースするようなプログラムを書いてみる
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に限らずパーサを書くには
モナドを使ったほうが便利で楽しく書けるんじゃないだろうか。
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