PCさえあればいい。
PCさえ
bleis-tift
2017年12月02日
・Scalaで簡単なミニ言語のパーサーの実装
所属と最近のお仕事
・所属: 関数プログラミング界隈
・最近のお仕事
・RaspberryPiにSchemeのせてルンバの制御
・F#でS式パーサーと評価器(簡易)の実装
いきなりですが
構造を持ったものはそこら中にある
世の中はパースすべきものであふれている
・ログとか
・設定ファイルとか
・ミニ言語(DSL)とか
・プログラミング言語とか
そこで正規表現ですよ!
・ネストする文法を正規表現で解決しようとしてはいけない
・→ 人が死ぬ
・そうじゃなくても複雑な正規表現には近寄りたくない
よし、文字列操作をゴリゴリ書こう!
・ごくシンプルなものならそれも可
・すぐに限界が訪れて人が死ぬ
そこでパーサーですよ!
・ソースコードの中身(文字列)からASTに
パーサーとは
・一次元的な構造(文字列とか)から構文木を作るもの
・例えば・・・
・ログの中身(文字列)からLogEntryオブジェクトに
・設定ファイルの中身(文字列)からConfigオブジェクトに
パーサーを作ろう!
パーサーを作る手段
・手書き
・パーサージェネレーター
・パーサーコンビネーター
まずはパーサーコンビネーターがおすすめ
パーサーコンビネーター?
・パーサーを作るためのライブラリ
・基本的なパーサー + パーサーを組み合わせるための関数
・モナ(ry
単なるライブラリなので
・部品が再利用可能
・部品単位でテスト可能
・外部ツールが不要
PCさえあればいい。
PCさえ
bleis-tift
2017年12月02日
あらため
ParserCombinator
さえあればいい。
bleis-tift
2017年12月02日
・コンパクトなうえ実装しやすい
・YAML
・仕様が大きすぎ・・・
JSONおススメ
何から始めればいいか
・電卓、数式
・伝統的だけどおもちゃ
・JSON
・最近よくある
パーサーを書くための3つのステップ
1. データ構造を用意します
2. パーサーの結果を用意したデータ構造に変換し
ます
3. できあがり!
データ構造
type Json =
| JNull
| JBool of bool
| JNumber of float
| JString of string
| JList of Json list
| JObject of Map<string, Json>
用意したデータ構造に変換
// この発表ではFParsecというライブラリを使います
let jnull = pstring "null" >>% JNull
let jtrue = pstring "true" >>% (JBool true)
let jfalse = pstring "false" >>% (JBool false)
let jnumber = pfloat |>> JNumber
let str = manyChars (noneOf """) |> between (pchar '"') (pchar '"')
let jstring = str |>> JString
let jvalue, jvalueRef = createParserForwardedToRef ()
let jlist =
sepBy jvalue (pchar ',')
|> between (pchar '[') (pchar ']') |>> JList
let jobject =
let jprop = str .>> pchar ':' .>>. jvalue
sepBy jprop (pchar ',')
|> between (pchar '{') (pchar '}')
|>> (Map.ofList >> JObject)
jvalueRef :=
choice [ jobject; jlist; jstring; jnumber; jtrue; jfalse; jnull ]
let json : Parser<Json, unit> = jvalue .>> eof
おしまい
let parse str =
match run json str with
| Success (res, _, _) -> res
| Failure (msg, _, _) -> failwithf "oops!: %s" msg
誰得はまりがちポイント
はまりがちなポイントを、時間が許す限り紹介し
ます。
・その場合、レキサーの仕事もパーサーでやる
空白の扱い
・パーサーの前段にレキサーを置くこともある
・レキサーは「文字列→トークン列」変換をする
・空白文字のスキップなどは、本来レキサーの仕事
・ライブラリによっては文字列しか扱えない
パーサーでの空白の読み飛ばし
・場当たり的に読み飛ばすと簡単に無限ループする
・各パーサーの「後ろの空白」を読み飛ばすようにする
・最後に全体のパーサーの「前の空白」を読み飛ばす
JSONパーサー(改良版)
let ws = spaces
let jnull = pstring "null" .>> ws >>% JNull
let jtrue = pstring "true" .>> ws >>% (JBool true)
let jfalse = pstring "false" .>> ws >>% (JBool false)
let jnumber = pfloat .>> ws |>> JNumber
let str = manyChars (noneOf """) |> between (pchar '"') (pchar '"') .>> ws
let jstring = str |>> JString
let jvalue, jvalueRef = createParserForwardedToRef ()
let jlist =
sepBy jvalue (pchar ',' .>> ws)
|> between (pchar '[' .>> ws) (pchar ']' .>> ws) |>> JList
let jobject =
let jprop = str .>> pchar ':' .>> ws .>>. jvalue
sepBy jprop (pchar ',' .>> ws)
|> between (pchar '{' .>> ws) (pchar '}' .>> ws)
|>> (Map.ofList >> JObject)
jvalueRef :=
choice [ jobject; jlist; jstring; jnumber; jtrue; jfalse; jnull ]
let json : Parser<Json, unit> = ws >>. jvalue .>> eof
文字列のエスケープ対応
// 文字列パーサー再掲
let str =
// エスケープシーケンスに対応していない
manyChars (noneOf """)
|> between (pchar '"') (pchar '"')
.>> ws
・これをそのまま書けばいい
エスケープ対応
・文字列の要素を2つに分ける
・普通の文字列要素
・エスケープシーケンス
・文字列は、上記2つの要素の繰り返し
こんな感じで
let str =
let nonEscaped = noneOf """ |> many1Chars
let escaped =
pchar '"' >>. anyOf @"""/bfnrt"
|>> function
| 'b' -> "b"
| 'f' -> "f"
| 'n' -> "n"
| 'r' -> "r"
| 't' -> "t"
| c -> string c
let elem = nonEscaped <|> escaped // four hex digitsなやつは省略
manyStrings elem |> between (pchar '"') (pchar '"') .>> ws
選択されない選択肢
整数と実数を別のデータとして扱いたい場合にどうするか
こう?
let integer = regex "([1-9][0-9]*|0)" |>> Int
let real = regex @"([1-9][0-9]*|0).[0-9]*" |>> Real
let number = integer <|> real
realがパース出来ない!
問題点
・12.3をパースすることを考える
・12まではintegerとして読める
・.でintegerが失敗
・.からrealで読もうとする
順番の変更
・numberの定義を修正
・integer <|> real から
・real <|> integer に
これで大丈夫?
integerがパース出来ない!
まだダメ
・12をパースすることを考える
・12までrealの整数部として読める
・.がないのでrealが失敗
・空の入力をintegerで読もうとする
問題点と解決方法
・FParsecではほかのパーサーが消費した入力は消える
・効率のための選択
・attemptで失敗したら戻るようにできる
・左括り出しについては略
これでOK
let integer = regex "([1-9][0-9]*|0)" |>> Int
let real = regex @"([1-9][0-9]*|0).[0-9]*" |>> Real
// attemptでrealが失敗したら、realが消費した文字を入力に戻す
let number = attempt real <|> integer
これをそのまま書いてみましょう
左再帰の文法
・数値と加算と減算のみの数式をパースしたい
・加算も減算も、左の項に加算や減算を許す
・10 - 2 - 4は((10-2) - 4)
・(10 - (2-4))ではない
データ構造
// Add/SubでExpr自体を使っている(再帰構造)
type Expr =
| Integer of int
| Add of Expr * Expr // 加算を表す
| Sub of Expr * Expr // 減算を表す
パーサー
let integer = pint32 .>> ws |>> Integer
let expr, exprRef = createParserForwardedToRef ()
let add =
tuple2 (expr .>> pchar '+' .>> ws) integer |>> Add
let sub =
tuple2 (expr .>> pchar '-' .>> ws) integer |>> Sub
exprRef := choice [attempt add; attempt sub; integer]
これに10 - 2 - 4を食わせると・・・
"
            _人人人人人人人人_
            > StackOverflow <
             ̄Y^Y^Y^Y^Y^Y^Y ̄"
問題点
・exprをパースしようとしてaddをパースしようとしてexprをパー
スしようとして・・・
・無限再帰!
・再帰は繰り返しで表現しましょう
左再帰の繰り返しへの変換
// 元のコードの意味(左再帰)
// expr ::= expr op integer
// | integer
// から、下記(繰り返し)に変換
// expr ::= integer (op integer)*
let expr =
integer .>>. (many (op .>>. integer))
|>> fun (x, xs) -> List.fold (fun x (f, y) -> f x y) x xs
and op =
anyOf "+-" .>> ws
|>> (function
| '+' -> fun a b -> Add(a, b)
| '-' -> fun a b -> Sub(a, b)
| other -> invalidOp ("invalid char: " + string other))
foldでの構造の変換が分かりにくい・・・
chainl1を使いましょう
// expr ::= integer (op integer)*
let expr = chainl1 integer op
and op =
anyOf "+-" .>> ws
|>> (function
| '+' -> fun a b -> Add(a, b)
| '-' -> fun a b -> Sub(a, b)
| other -> invalidOp ("invalid char: " + string other))
便利!
まとめ
・カジュアルにパーサーを書こう
・パーサーコンビネーターはお手軽
・パーサーコンビネーターは便利
・年末年始はパーサーを書いて過ごしましょう

PCさえあればいい。