昨日 面倒くさいパーサの実装もDSLで書くだけ!そう、Scalaならね - Qiita という記事を書いたが、パーサコンビネータではまった点があった。
パーサコンビネータの |
(alternative) と ^^
(eye brows)の組み合わせで、最後の alternative にしか eye brows の関数が適用されないという問題です。
このサンプルコードを御覧ください。入力 "1"
は Number(1)
に変換されますが、入力 "0"
は変換されません。
import scala.util.parsing.combinator._
case class Number(number: String)
object Tokenize extends RegexParsers {
def number = "0" | "1" ^^ { Number(_) }
def apply(input: String) = parseAll(number, input)
}
println(Tokenize("0")) //> [1.2] parsed: 0
println(Tokenize("1")) //> [1.2] parsed: Number(1)
これは、^^
オペレータが "1"
のメソッドであるためのようです。カッコを補って表現すると次のような解釈になるわけですね。
def number = "0" | "1".^^({ Number(_) })
この解釈を回避するには2つ方法があります。
- alternativeそれぞれに
^^
をつける - alternativeをすべて
( )
で囲んで評価の順番を変える
1の例
import scala.util.parsing.combinator._
case class Number(number: String)
object Tokenize extends RegexParsers {
def number = "0" ^^ { Number(_) } | "1" ^^ { Number(_) }
def apply(input: String) = parseAll(number, input)
}
println(Tokenize("0")) //> [1.2] parsed: Number(0)
println(Tokenize("1")) //> [1.2] parsed: Number(1)
2の例
import scala.util.parsing.combinator._
case class Number(number: String)
object Tokenize extends RegexParsers {
def number = ("0" | "1") ^^ { Number(_) }
def apply(input: String) = parseAll(number, input)
}
println(Tokenize("0")) //> [1.2] parsed: Number(0)
println(Tokenize("1")) //> [1.2] parsed: Number(1)
|
と ^^
の評価の順番を常に意識していないとはまるので、|
を使うときはいつもカッコをつけるようにしておいたほうが考えなくていいので楽かもしれませんね。