LL脳がscalaの勉強を始めたよ その110


Scalaコップ本の31章の続きをやっていきますよ(`・ω・´)31章はパーサーコンビネーターで言語内DSLをゴニョゴニョしてますデス。今回は基本正規表現パーサーからやっていきますYO!

基本正規表現パーサー

前回やった算術式パーサーでは、JavaTokenParsersトレイトを継承することでfloatingPointNumbersというJava浮動小数点の書式を解析するパーサーを利用したのですが、Javaとは異なる書式の数値を解析しなければならないときは正規表現パーサーを使ってゴニョゴニョするのがよさそうです。

正規表現パーサーでは指定された正規表現によってマッチングして文字列を解析する、と…とりあえずサンプルを見てみますかね

import scala.util.parsing.combinator._
// RegexParsersトレイトを継承して正規表現パーサーを実現しますよ
object MyParsers extends RegexParsers {
  // 文字列と_から開始される英数字で表現された文字列
  // (\wは[0-9a-zA-Z_])にマッチする正規表現デス
  val ident:Parser[String] = """[a-zA-Z_]\w*""".r
}

それでは実行してみますかね(`・ω・´)

// マッチしました
scala> MyParsers.parseAll(MyParsers.ident, "abc")  
res12: MyParsers.ParseResult[String] = [1.4] parsed: abc

// マッチしませんでした
scala> MyParsers.parseAll(MyParsers.ident, "123")
res13: MyParsers.ParseResult[String] = 
[1.1] failure: string matching regex `[a-zA^Z_]\w*' expected but `1' found

123
^

Scalaのパーサー用トレイトはイロイロと種類が豊富ですな。こいつらパーサー用トレイトはscala.util.parsing.combinatorパッケージに階層的に定義されているみたいです(`・ω・´)

他のパーサーの使用例:JSON

Scalaのパーサートレイトを利用することでJSON形式のデータもパースすることファ出来るみたいですな(`・ω・´)ちなみにJSONの構文は次のように表現できるみたいです

 // 各値は次のソレゾレで表現されます
 value ::= obj | arr | stringLiteral | floatingPointNumber | "null" | "true" | "false".
 // オブジェクトはmembersとして表現します
 obj ::= "{" [members] "}".
 // 配列も扱えますデス
 arr ::= "[" [values] "]".
 // 辞書的データとして複数の値をもてますな
 members ::= member {"," member}.
 // 辞書的に使う場合のkey - value形式ですな
 member ::= stringLiteral ":" value.
 // 配列は値列挙のシーケンスです
 values ::= value {"," value}.


とりあえずJSON形式データのサンプルを書いてみますよ

// アドレス帳データをJSONで表現シマス
{
  "address book":{
    "name":"John Smith",
    "address":{
      "street":"10 Market Street",
      "city":"San Francisco, CA",
      "zip":94111
    },
    "phone numbers":[
      "408 338-4238",
      "408 111-6892"
    ]
  }  
}

それではScalaのパーサーコンビネーターを使って上記JSONデータを解析するパーサーを書いてみますよ。前回と同様に文法定義を機械的に翻訳してるだけですが(´・ω・`)

import scala.util.parsing.combinator._
class JSON extends JavaTokenParsers {
  def value:Parser[Any] = obj | arr | stringLiteral | floatingPointNumber
                                       "null" | "true" | "false"
  // カンマ区切りのシーケンスをrepsepを使って解析します
  def obj:Parser[Any] = "{"~repsep(member, ",")~"}"
  def arr:Parser[Any] = "["~repsep(value, ",")~"]"
  def member:Parser[Any] = stringLiteral~":"~value    
}

んじゃ、実行してみます。今回はJSONファイル(address-book.json)を読み込んでパースするような動作にしてみますよ

import java.io.FileReader
object ParseJSON extends JSON{
  def main(args:Array[String]){
    val reader = new FileReader(args(0))
    println(parseAll(value, reader))
  }
}

ではコンパイルして実行しますです

// コンパイルします
% scalac ParseJSON.scala

// 実行しますよ
% scala ParseJSON address-book.json
[16.1] parsed: (({~List((("address book"~:)~(({~List((("name"~:)~"John Smith"), (("address"~:)~(({~List((("street"~:)~"10 Market Street"), (("city"~:)~"San Francisco, CA"), (("zip"~:)~94111)))~})), (("phone numbers"~:)~(([~List("408 338-4238", "408 111-6892"))~]))))~}))))~})

おお!出来ました。Scalaを使うとJSONのパースもあっさりと出来るみたいですネ(`・ω・´)

パーサーの出力

様々な形式の文字列をパース出来るようにはなったのですが、出力結果がイマイチよくわからないのでこれらを利用する方法についてみていきますよ(`・ω・´)

パース出力のルール

パース結果を利用するにはコンビネータフレームワークの個々のパーサーが結果値として何を返すのかを知っておく必要があるみたいです。ちなみに結果値のルールは次のようになるみたいです

  • "{", ":", "null"等の文字列形式で書かれたパーサーは解析した文字列自体を返す
  • """[a-zA-Z_]\w*""".rのような正規表現パーサーも解析した文字列自体を返す。またjavaTokenParsersを継承するstringLiteralやfloatingPointNumberも同様
  • P~Qという逐次合成はPとQの結果値の両方を返す。これらの結果値は~という名前のケースクラスのインスタンスとして返される
    • Pが"true"でQが"?"を返す場合、P~Qは〜(”true”, "?")を返して(true~?)を出力される
  • P | Qという選択はPとQとで成功した方の結果値を返す
  • 反復のrep(P)やrepsep(P, separator)はPに含まれる全ての要素の結果から作ったリストを返す
  • オプションのopt(P)はScalaのOption型インスタンスを返す。Pが成功した場合は結果値のRをラップしたSome(R)、失敗した場合はNoneになる
Scalaで扱いやすい形式への変換

上記ルールに沿って考えるとJSON形式のデータをパースした結果の出力は理解できるものの、Scala上で扱うのはちょっと困難デス(´・ω・`)例えば次のような形に変換できれば扱いやすくなりそうデスな

  • JSONオブジェクトはMap[String, Any]型のScalaマップとして表現する。全てのメンバーはマップ内のキー・値の対として表現
  • JSON配列はList[Any]型のScalaリストとして表現
  • JSON文字列はScalaのStringとして表現
  • JSON数値リテラルScalaのDoubleとして表現
  • true, false, nullの値は同名のScalaの値として表現

上記のような変換を行うためには^^というパーサーのためのコンビネーターを使って処理結果を変換する必要があるみたいです。

^^演算子はPをパーサー、fを変換関数としたときにP ^^ fの形式で扱うみたいです。このときPというパーサーの結果がRだった場合のP ^^ fの結果はf(R)として扱われますデス(`・ω・´)

とりあえずサンプルを試してっますかね。まずは浮動小数点を解析してScalaのDouble型に変換するパーサーです

floatingPointNumber ^^ (_.toDouble)

また、"true"という文字列をScalaのBoolean型のtrueに変換するパーサーの場合は次のようになりますな(`・ω・´)

"true" ^^ (x => true)

ちょっぴり複雑な例としてScalaのMap型の結果を返す変換を書いてみますデス

def obj:parser[map[String, Any]] = 
  "{"~repsep(member, ",")~"}" ^^ { case "{"~ms~"}" => Map() ++ ms }

上記"{"~ms~"}"のようなパターンで使われる~演算子は、同名の~ケースクラスのインスタンスとして結果値を返すのですが~ケースクラスの定義は次のようになっておりますね

case class ~[+A, +B](x:A, y:B){
  override def toString = "(" + x + "~" + y + ")"
}

ちなみにこの演算子を使った冗長表現で"{"~ms~"}"を記述すると~( ~( "{", ms), "}")...と非常うに読みづらくなるです(´・ω・`)

また、上記のパーサを~>と<~という2つのパーサーコンビネーターを使うことでより簡潔に記述することが出来るみたいです。

def obj:parser[Map[String, Any]] =
  "{"~> repsep(member, ",")  <~"}" ^^ (Map() ++ _)

~>は右被演算子の解析結果だけ残して残りを捨て、<~は左被演算子の解析結果だけ残して残りを捨てりコンビネータみたいです

JSONパーサーをアップデートしてやります

それでは意味のある結果値を返すJSONパーサーを書いてみますよ

import scala.util.parsing.combinator._
class JSON1 extends JavaTokenParsers {
  def obj:Parser[Map[String, Any]] =
    "{"~> repsep(member, ",") <~"}" ^^ (Map() ++ _)
  def arr:Parser[List[Any]] = 
    "["~> repsep(value, ",") <~"]"
  def member:Parser[(String, Any)] =
    stringLiteral~":"~value ^^ { case name~":"~value => (name, value) }
  def value:Parser[Any] = (
    obj
    | arr
    | stringLiteral
    | floatingPointNumber ^^ (_.toDouble)
    | "null" ^^ (x => null)
    | "true" ^^ (x =>true)
    | "false" ^^ (x => false)
  )
}

いじょー

とりあえず今回はココマデです。次回はパーサーコンビネーターの実装からやっていきますね(`・ω・´)