PCさえあればいい。

303 views

Published on

NGK2017Bでの発表資料です。

Published in: Technology
  • Be the first to comment

PCさえあればいい。

  1. 1. PCさえあればいい。 PCさえ bleis-tift 2017年12月02日
  2. 2. ・Scalaで簡単なミニ言語のパーサーの実装 所属と最近のお仕事 ・所属: 関数プログラミング界隈 ・最近のお仕事 ・RaspberryPiにSchemeのせてルンバの制御 ・F#でS式パーサーと評価器(簡易)の実装
  3. 3. いきなりですが
  4. 4. 構造を持ったものはそこら中にある 世の中はパースすべきものであふれている ・ログとか ・設定ファイルとか ・ミニ言語(DSL)とか ・プログラミング言語とか
  5. 5. そこで正規表現ですよ! ・ネストする文法を正規表現で解決しようとしてはいけない ・→ 人が死ぬ ・そうじゃなくても複雑な正規表現には近寄りたくない
  6. 6. よし、文字列操作をゴリゴリ書こう! ・ごくシンプルなものならそれも可 ・すぐに限界が訪れて人が死ぬ そこでパーサーですよ!
  7. 7. ・ソースコードの中身(文字列)からASTに パーサーとは ・一次元的な構造(文字列とか)から構文木を作るもの ・例えば・・・ ・ログの中身(文字列)からLogEntryオブジェクトに ・設定ファイルの中身(文字列)からConfigオブジェクトに
  8. 8. パーサーを作ろう!
  9. 9. パーサーを作る手段 ・手書き ・パーサージェネレーター ・パーサーコンビネーター まずはパーサーコンビネーターがおすすめ
  10. 10. パーサーコンビネーター? ・パーサーを作るためのライブラリ ・基本的なパーサー + パーサーを組み合わせるための関数 ・モナ(ry
  11. 11. 単なるライブラリなので ・部品が再利用可能 ・部品単位でテスト可能 ・外部ツールが不要
  12. 12. PCさえあればいい。 PCさえ bleis-tift 2017年12月02日
  13. 13. あらため
  14. 14. ParserCombinator さえあればいい。 bleis-tift 2017年12月02日
  15. 15. ・コンパクトなうえ実装しやすい ・YAML ・仕様が大きすぎ・・・ JSONおススメ 何から始めればいいか ・電卓、数式 ・伝統的だけどおもちゃ ・JSON ・最近よくある
  16. 16. パーサーを書くための3つのステップ
  17. 17. 1. データ構造を用意します
  18. 18. 2. パーサーの結果を用意したデータ構造に変換し ます
  19. 19. 3. できあがり!
  20. 20. データ構造 type Json = | JNull | JBool of bool | JNumber of float | JString of string | JList of Json list | JObject of Map<string, Json>
  21. 21. 用意したデータ構造に変換 // この発表では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
  22. 22. おしまい let parse str = match run json str with | Success (res, _, _) -> res | Failure (msg, _, _) -> failwithf "oops!: %s" msg
  23. 23. 誰得はまりがちポイント
  24. 24. はまりがちなポイントを、時間が許す限り紹介し ます。
  25. 25. ・その場合、レキサーの仕事もパーサーでやる 空白の扱い ・パーサーの前段にレキサーを置くこともある ・レキサーは「文字列→トークン列」変換をする ・空白文字のスキップなどは、本来レキサーの仕事 ・ライブラリによっては文字列しか扱えない
  26. 26. パーサーでの空白の読み飛ばし ・場当たり的に読み飛ばすと簡単に無限ループする ・各パーサーの「後ろの空白」を読み飛ばすようにする ・最後に全体のパーサーの「前の空白」を読み飛ばす
  27. 27. 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
  28. 28. 文字列のエスケープ対応 // 文字列パーサー再掲 let str = // エスケープシーケンスに対応していない manyChars (noneOf """) |> between (pchar '"') (pchar '"') .>> ws
  29. 29. ・これをそのまま書けばいい エスケープ対応 ・文字列の要素を2つに分ける ・普通の文字列要素 ・エスケープシーケンス ・文字列は、上記2つの要素の繰り返し
  30. 30. こんな感じで 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
  31. 31. 選択されない選択肢 整数と実数を別のデータとして扱いたい場合にどうするか
  32. 32. こう? let integer = regex "([1-9][0-9]*|0)" |>> Int let real = regex @"([1-9][0-9]*|0).[0-9]*" |>> Real let number = integer <|> real
  33. 33. realがパース出来ない! 問題点 ・12.3をパースすることを考える ・12まではintegerとして読める ・.でintegerが失敗 ・.からrealで読もうとする
  34. 34. 順番の変更 ・numberの定義を修正 ・integer <|> real から ・real <|> integer に これで大丈夫?
  35. 35. integerがパース出来ない! まだダメ ・12をパースすることを考える ・12までrealの整数部として読める ・.がないのでrealが失敗 ・空の入力をintegerで読もうとする
  36. 36. 問題点と解決方法 ・FParsecではほかのパーサーが消費した入力は消える ・効率のための選択 ・attemptで失敗したら戻るようにできる ・左括り出しについては略
  37. 37. これで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
  38. 38. これをそのまま書いてみましょう 左再帰の文法 ・数値と加算と減算のみの数式をパースしたい ・加算も減算も、左の項に加算や減算を許す ・10 - 2 - 4は((10-2) - 4) ・(10 - (2-4))ではない
  39. 39. データ構造 // Add/SubでExpr自体を使っている(再帰構造) type Expr = | Integer of int | Add of Expr * Expr // 加算を表す | Sub of Expr * Expr // 減算を表す
  40. 40. パーサー 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]
  41. 41. これに10 - 2 - 4を食わせると・・・
  42. 42. "             _人人人人人人人人_             > StackOverflow <              ̄Y^Y^Y^Y^Y^Y^Y ̄"
  43. 43. 問題点 ・exprをパースしようとしてaddをパースしようとしてexprをパー スしようとして・・・ ・無限再帰! ・再帰は繰り返しで表現しましょう
  44. 44. 左再帰の繰り返しへの変換 // 元のコードの意味(左再帰) // 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))
  45. 45. foldでの構造の変換が分かりにくい・・・
  46. 46. 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))
  47. 47. 便利!
  48. 48. まとめ ・カジュアルにパーサーを書こう ・パーサーコンビネーターはお手軽 ・パーサーコンビネーターは便利 ・年末年始はパーサーを書いて過ごしましょう

×
Save this presentationTap To Close