2013年9月のアクセス解析
こうテーブルのヘッダ表示が残念な感じ
続きを読むFParsecでパーサコンビネータのお勉強
はじめに
この記事はF# Advent Calendar 2013の18日目のです。ひとつ前の記事は@kos59125さんのDeedle で遊ぼう - 捨てられたブログです。
パーサコンビネータとは
文字列の構造を解析するプログラムをパーサと言いますが、パーサコンビネータはその中の一種です。単純なパーサ関数を合成することで複雑なパーサを作り上げることができます。HaskellのParsecが有名ですが、FParsecはそれをF#に移植したものです。チュートリアルが和訳されています(#112 FParsec日本語チュートリアル « F# « a wandering wolf)ので、どういうものか知りたい方はそちらを。
パースする
それでは、具体的なパーサとして浮動小数点数をパースするpfloatを見ていきます。
pfloatは CharStream<'u> 型の引数を取り Reply
ここで、 'u という型はUser Stateの型を表しているのですが、この記事ではUser Stateは扱わないためこれ以降は無視します。User Stateについてはこの記事に詳しく書かれていますFParsecで遊ぶ - 2つのアンコール
さて、まずCharStream型ですが、これはパース対象の文字列の入れ物のようなものです。文字列の先頭から順番に文字を読みだすことができるのですが、そのとき「どこまで読んだのか」という状態を保持しておくことができます。
次にReply
- Error
- Result
- Status
という3つのフィールドを持つ構造体です。
pfloat関数はCharStreamのまだ読まれていない部分の先頭から、浮動小数点数っぽい文字の並びがあるかどうかを、既読位置を変化させずに調べます。そして、成功したときはあらためて文字列を読み込んで(このとき既読位置が変化します)、数値に変換してReplyのResultフィールドに入れます。
パース前 123.45abcde ^ パース後 123.45abcde ^
もしパースに失敗したときは、Errorフィールドにエラーメッセージを入れます。(この時はCharStreamの既読位置は変化しません)
パース前 abcabcde ^ パース後 abcabcde ^
そしてStatusフィールドには、成功したか、あるいは失敗したかを表す列挙体が代入されます。
目的の値を得るには返り値のStatusフィールドを調べて、成功ならResult、失敗ならErrorをそれぞれ調べればいいのですが、あとでもっといい方法がでてきます。
パーサを合成する
pfloat関数だけでは、「[]で囲まれた小数をパースしたいんだ」とかのニーズに応えることができません。そういうときは、パーサ同士を合成することで新しいパーサを作り出します。例えば今言ったようなパーサ関数は
let floatBetweenBrackets = pstring "[" >>. pfloat .>> pstring "]" (* (pstring "[") で"["から始まる文字列をパースする関数が作れる *)
とすることで作ることができます。
ここで注目したいのは (pstring "[") と pfloat という2つのパーサ関数を引数に取る(>>.)という演算子です。
この演算子はParser型(つまりパーサ関数と同じ型)の関数を返すのですが、その関数がどういう働きをするかというと、まず一つ目の関数で与えられたCharStream型の値をパースして、それが成功したら二つ目のパーサで続けてパースします。両方成功したら後の方のパーサで得られた結果を返し、失敗したらパーサから得られたエラーメッセージを適切に返します。
成功 成功 一つ目のパース -----> 二つ目のパース -------> 二つ目のパーサの結果を返す | | +--> 結果をErrorに +-> 結果をErrorに 失敗 失敗
これが(>>.)演算子のおおまかな働きです。(.>>)も大体同じです。(一つ目のパーサの結果を返します)ここで重要なのは、これらの演算子がParser型を引数に取り、同じParser型を返すということです。それはつまりパーサを合成した結果得られたパーサをまた他のパーサと合成することができるということです。FParsecでパーサの合成を行う演算子はほとんどが、このようにパーサを受け取りパーサを返します。いくら合成したとしても、得られたパーサは新しいパーサの材料となるのです!たとえ単純なパーサや演算子しかなかったとしても、可能性は無限に広がります。
パーサを走らせる
さて、今までパーサの本体を見てきましたが、これは直接使うにはあまり使い勝手がよくありません。CharStream型のインスタンスを生成しなければなりませんし、返り値から欲しい値を取り出すのも面倒です。そこで、FParsecにはrunという関数があります。
val run: Parser<'a, unit> -> string -> ParserResult<'a,unit>
パーサと文字列を引数にとり、ParserResult型という判別共用体を返します。ParserResult型はSuccessとFailureという2値を取り、それぞれ成功時の値、失敗時のエラーメッセージを保持します。
run関数にパーサ関数を渡した時点で新しい関数が生成されているとみることもできます。これもコンビネータ(関数を受け取り関数を返す関数)と言うことができるでしょう。
まとめ
ほんのちょっとですがFParsecの中身について見てきました。小さな関数でも組み合わさることで大きな仕事をすることができるようになるというのはステキですね。合成の対象となる関数と、その結果生成される関数の型をそろえることで何度でも合成可能になることは、自分でコンビネータを作ろうとするとき*1にも意識しておこうと思います。
拙い文章にここまで付き合ってくださり、本当にありがとうございました。
*1:たぶんないと思いますが
PowerShellの関数の引数を補完する
PowerShellでこんな関数を書いたとして
function Jump-Location{ param($Target) $jumpList = @{"SendTo"=([Environment]::GetFolderPath("sendto")); "StartUp"=([Environment]::GetFolderPath("startup"))} Set-Location -Path $jumpList[$Target] }
こんな感じで使うのですが
Jump-Location -Target StartUp # スタートアップフォルダにChange Directoryする Jump-Location SendTo # -Targetは省略できる
ここでStartUpとかSendToとかを補完できたら素敵ですよね。
引数にValidateSet属性を付けるとそれが可能になります。
function Jump-Location{ param([ValidateSet("SendTo","StartUp")]$Target) # ここを変える $jumpList = @{"SendTo"=([Environment]::GetFolderPath("sendto")); "StartUp"=([Environment]::GetFolderPath("startup"))} Set-Location -Path $jumpList[$Target] }
[ValidateSet(配列)$変数名]とすることで引数が取れる値を配列の要素に限定することができます。(それ以外の値が入力されるとエラーになります)そして指定した値はTABキーで補完できるようになります。
Jump-Location -Target # ここでTABを押すとSendToやStartUpが補完される Jump-Location # -Targetを省略しても補完される
ステキ!
ところで、Hashのキーを変数に取り出して指定できたらもっと素敵なのですが、
$jumpList = @{"SendTo"=([Environment]::GetFolderPath("sendto")); "StartUp"=([Environment]::GetFolderPath("startup"))} $keys = $jumpList.Keys function Jump-Location{ param([ValidateSet($keys)]$Target) # param文は関数の先頭にしか置けない? Set-Location -Path $jumpList[$Target] }
としても、
発生場所 行:6 文字:24 + param([ValidateSet($keys)]$Target) + ~~~~~ パラメーター属性は、定数またはスクリプト ブロックである必要があります。 + CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException + FullyQualifiedErrorId : ParameterAttributeArgumentNeedsToBeConstantOrScriptBlock
とダメ出しを受けます。*1がっかり。
*1:Set-Variable keys $jumpList-Option Constant も試してみたけどダメだった