読者です 読者をやめる 読者になる 読者になる

Scala標準のパーサーライブラリ

Scala

ScalaのParser Combinatorsで遊んでます。
Boost.Spirit.Qiのときにもやった"(123)"というカッコに囲まれた値を取り出す処理を書きます。

import scala.util.parsing.combinator.syntactical._

object Main extends StandardTokenParsers {
    lexical.delimiters ++= List("(", ")")

    def parse = "(" ~ numericLit ~ ")" ^^ { case a ~ b ~ c => b }

    def main(args: Array[String]) {
        val s : String = "(123)";

        parse(new lexical.Scanner(s)) match {
            case Success(result, _) => println(result)
            case Failure(msg, _) => println("failure : " ++ msg)
            case Error(msg, _) => println("error : " ++ msg)
        }
    }
}
123


まず、StandardTokenParsersを継承したクラスでパーサーを書いていきます。
この部分がパース式:

"(" ~ numericLit ~ ")"

この式によって、"(", "123", ")"の3つのパーツに分けられます。
numericLitは、文字列中の整数にマッチするパーサーです。


次に、^^演算子でパース成功時に呼び出す関数を指定します。
(Boost.Spirit.Qiで言うところのセマンティックアクションです。)
カッコ(aとc)はいらないのでパターンマッチでbだけ取り出してます。

^^ { case a ~ b ~ c => b }


あとは呼び出し側で、パターンマッチによって成功か失敗かを判定し、結果を表示しておしまいです。

parse(new lexical.Scanner(s)) match {
    case Success(result, _) => println(result)
    case Failure(msg, _) => println("failure : " ++ msg)
    case Error(msg, _) => println("error : " ++ msg)
}

処理の流れはこんな感じです。


ここからは短くしていくお話。
以下の部分はちょっと冗長です。

^^ { case a ~ b ~ c => b }

まず、aとcは名前を付ける必要がないのでワイルドカードにできます。

^^ { case _ ~ x ~ _ => x }

全コード:

import scala.util.parsing.combinator.syntactical._

object Main extends StandardTokenParsers {
    lexical.delimiters ++= List("(", ")")

    def parse = "(" ~ numericLit ~ ")" ^^ { case _ ~ x ~ _ => x }

    def main(args: Array[String]) {
        val s : String = "(123)";

        parse(new lexical.Scanner(s)) match {
            case Success(result, _) => println(result)
            case Failure(msg, _) => println("failure : " ++ msg)
            case Error(msg, _) => println("error : " ++ msg)
        }
    }
}

不要な情報は減りましたが、長さは変わってないです。
パターンマッチの時点で不要な情報をはじくのではなく、パース式の段階で「パースはするけどいらない」という指定ができれば短くなりそうです。
(Boost.Spirit.Qiではomitディレクティブに当たります。)


ドキュメント上見つけることはできませんでしたが、実装上では~>演算子、<~演算子によってそういった指定ができるそうです。

def parse = "(" ~> numericLit <~ ")" ^^ { case x => x }

全コード:

import scala.util.parsing.combinator.syntactical._

object Main extends StandardTokenParsers {
    lexical.delimiters ++= List("(", ")")

    def parse = "(" ~> numericLit <~ ")" ^^ { case x => x }

    def main(args: Array[String]) {
        val s : String = "(123)";

        parse(new lexical.Scanner(s)) match {
            case Success(result, _) => println(result)
            case Failure(msg, _) => println("failure : " ++ msg)
            case Error(msg, _) => println("error : " ++ msg)
        }
    }
}

パターンマッチの段階でカッコはもはや渡ってこないので、

case x => x

だけで済むようになりました。


分解する必要がなくなったので、パターンマッチ自体いらないですね。
最終的なコードは以下のようになります:

import scala.util.parsing.combinator.syntactical._

object Main extends StandardTokenParsers {
    lexical.delimiters ++= List("(", ")")

    def parse = "(" ~> numericLit <~ ")"

    def main(args: Array[String]) {
        val s : String = "(123)";

        parse(new lexical.Scanner(s)) match {
            case Success(result, _) => println(result)
            case Failure(msg, _) => println("failure : " ++ msg)
            case Error(msg, _) => println("error : " ++ msg)
        }
    }
}


これで90%くらい満足ですが、デリミタの定義がないといけないというのがよくわかってない点です。
またくわしく調べていこうと思います。