構文解析

Scalaのparser combinatorを使って、簡単な構文解析を試してみる。
以下は、よくある四則演算に関数(sin,cos,exp,ln)を追加した例。関数の直前に単項演算子の'-'が来るときの対処がちょっと苦しいが、自然な形で書ける。(floatingPointNumberの方は、それ自身に単項演算子の'-'が含まれている)
'~'がparser combinatorの役割を果たすメソッドで、左右のparserを組み合わせてparserを作り出す。(フォントによって、'~'とマイナス記号が区別し辛いかもしれない)

import scala.util.parsing.combinator._
class FuncArith extends JavaTokenParsers {
  def expr: Parser[Any]   = term   ~ rep("+" ~ term | "-" ~ term)
  def term: Parser[Any]   = factor ~ rep("*" ~ factor | "/" ~ factor)
  def factor: Parser[Any] = floatingPointNumber | opt("-") ~ func_apply | "(" ~ expr ~ ")"
  def func_apply: Parser[Any] = func ~ "(" ~ expr ~ ")"
  def func: Parser[String] = "sin" | "cos" | "exp" | "ln"
}

"+" ~ term の中の"+"のように、parserではなく文字列リテラルを直接書けるのは、JavaTokenParsersのスーパークラスであるRegexParsersの中で、以下のように暗黙の型変換(String => Parser[String])が定義されているため。

 implicit def literal(s: String): Parser[String] = ...

構文解析後の処理を追加した例を以下に示す。
値を実際に計算して表示する。

$ scala FuncArith "1 + sin(3.14/2)"
input: 1 + sin(3.14/2)
result: [1.16] parsed: 1.9999996829318345

"^^"演算子の左側のparserの解析結果を入力として、^^の右側の処理が実行される。

import scala.util.parsing.combinator._

object FuncArith extends JavaTokenParsers {
  type R = Double

  def expr: Parser[R] = term ~
            rep("+" ~> term ^^ (t2 => (t1: R) => t1 + t2) |
                "-" ~> term ^^ (t2 => (t1: R) => t1 - t2)) ^^ {
    case first ~ fs => fs.foldLeft(first) { (x, f) => f(x) }
  }

  def term: Parser[R] = factor ~
            rep("*" ~> factor ^^ (f2 => (f1: R) => f1 * f2) |
                "/" ~> factor ^^ (f2 => (f1: R) => f1 / f2)) ^^ {
    case first ~ fs => fs.foldLeft(first) { (x, f) => f(x) }
  }

  def factor: Parser[R] = floatingPointNumber ^^ (_.toDouble)   |
      opt("-") ~ func_apply ^^ { case Some(_) ~ f => -1.0 * f
                                 case None    ~ f =>  f       } |
      "(" ~> expr <~ ")"

  def func_apply: Parser[R] = func ~ "(" ~ expr ~ ")" ^^ {
    case func ~ _ ~ expr ~ _ => func(expr)
  }

  def func: Parser[R => R] = "sin" ^^^ Math.sin _  |
                             "cos" ^^^ Math.cos _  |
                             "exp" ^^^ Math.exp _  |
                             "ln"  ^^^ Math.log _ 

  def main(args: Array[String]) {
    val s = args(0)
    println("input: " + s)
    println("result: " + parseAll(expr, s))
  }
}


適当なLispの例。

import scala.util.parsing.combinator._

object Lisp extends JavaTokenParsers {
  abstract class E
  case class Lsym(s: String)  extends E
  case class Lnum(i: Int)  extends E
  case class Llist(l: List[E]) extends E

  lazy val list: Parser[E] = "(" ~> rep(expr) <~ ")" ^^ { x => Llist(x) }
  lazy val num: Parser[E]  = """[+-]?[0-9]+""".r ^^ { s => Lnum(s.toInt) }
  lazy val sym: Parser[E]  = """[_a-zA-Z][_a-zA-Z0-9]+""".r ^^ { s => Lsym(s)}
  lazy val expr: Parser[E] = num | sym | list

  def main(args: Array[String]) {
    println(parseAll(expr, "(abc -2 3)"))
  }
}