yunomuのブログ

趣味のこと

簡易設定ファイルパーサジェネレータ

なまえがややこしい。

設定ファイルって面倒くさいからあんまし書きたくなくて、だいたいはコマンドライン引数で済ませる方向で生きてきました。
が、それはそれで面倒だったりして、いやまあ結構便利なライブラリがそれぞれの言語や環境ごとにあったりするのでいいといやいいんだけど、今回はそれは別として、
やっぱり設定ファイルを書かなきゃいけない……というか欲しい場面というのはあるのです。

で、私がよく使う設定ファイルの形式というのがまずあります。
こういうの。

title: wakaruwa
rssuri: http://localhost/rss.xml
keywords: kaede, anzu, kirari

コロン区切りのkey-value形式で、値の部分は文字列だったり、URIだったり、コンマ区切りのリストとかも書けるといいなぁみたいな。

RubyだったらちゃんとYAMLにして{:title => "wakaruwa", :rssuri => "http://localhost/rss.xml", :keywords => ["kaede", "anzu", "kirari"]}というHashを作ってあげればいい。これは容易い。容易い、けどURIのvalidationとか、リストのYAML表現とか、数値は文字列じゃなくてちゃんと数値型で欲しいとか、そういう細かいことを言い出すと面倒くさい。面倒くさいけどほしいものはほしい。

今回はHaskellで書くわけだし、そうなると上記のコードはData.Mapになりそうなもんだけど、Valueの型がバラバラだとまた面倒くさい。こういうの定義する?

data ConfigValue = ConfString String | ConfURI URI | ConfList ConfigValue

これでもまあいけるんだけど、keyの名前を文字列で渡すというのはちょっと、実行時エラーが出そうでいかにもHaskell的ではない。

やっぱりrecordを使いたいですよね。
つまりこういうのを自動生成したい。

data Config = Config {title :: String, rssuri :: URI, keywords :: [String]}

さらにこのレコードを構築する設定ファイルのパーサも自動生成したい。
いや、パーサは何度も書き直して、どうにか楽に書けるようにならないかと模索していたんですが、どうにもならなくて、どうしても上記の例で言うところの"title"とか"rssuri"とかいう文字列がソースコード中に複数回出てくるようになってしまって、エントリを一つ追加するのも本当に苦痛で仕方なくて。

ただ、レコードの名前とかフィールド名とか型とかはコンパイル時に決まるものだし、レコードって不完全な状態というか一部の値が入ってない状態とかはありえないし、とかそういう制約があって、型を含めた柔軟な自動生成とか初期化というのは非常に難易度が高い。
んですが、Template Haskellを使えばできそうな気がする。なんせHaskell構文木構築をHaskellで書けるわけだから、そりゃ文法的にできることは材料さえ揃っていれば全部できるんだろう。そこから先、コンパイルできるかどうかとかは知らないけども。

というわけで材料集め。
だいたいこのくらいの情報があれば自動生成できる気がする。

Config
    title String
    rssuri URI
    keywords [String]

レコード名があって、key名とvalueの型がある。

この情報をConfTmpという型にまとめる。

type ConfTmp = (String, [ConfLine])
type ConfLine = (String, ConfType)
data ConfType = ConfString | ConfURI | ConfList ConfType

レコードの名前があって、それにエントリの名前と型情報の組のリストがくっついてる。

これを使ってレコード型の定義を作る関数がmkRecord。

mkRecord :: Name -> [ConfLine] -> DecQ
mkRecord recName confLines =
    dataD (cxt []) recName [] [rec] [''Show]
  where
    rec = recC recName $ map confVSType confLines

    confVSType :: ConfLine -> VarStrictTypeQ
    confVSType (name, ctype) = confVSType' name $ confTypeQ ctype

    confVSType' :: String -> TypeQ -> VarStrictTypeQ
    confVSType' name typeq =
        varStrictType (mkName name) $ strictType notStrict typeq

    confTypeQ :: ConfType -> TypeQ
    confTypeQ ConfString = [t|String|]
    confTypeQ ConfURI = [t|String|]
    confTypeQ (ConfList ctype) = [t|[$(confTypeQ ctype)]|]

まあこの辺はTemplate Haskell(TH)を知ると結構自然に書けるようになります。

この辺はどうでもいいんですが、そんなこんなでできた設定ファイルパーサジェネレータがこちらになります。
呼び出し部分: nicodicbot/src/Config.hs at blog20120902 · yunomu/nicodicbot · GitHub
本体: nicodicbot/src/Config at blog20120902 · yunomu/nicodicbot · GitHub

やってることは以下のとおり。

  1. 必要なパーサの部品を作る(Config/Lib.hs)
  2. 設定のvalueの型ごとのパーサを作る(cv_string, cv_uri, cv_list,…)
  3. 以下TH
    1. recordを生成する
    2. recordをData.Defaultのインスタンスにする(後のStateのため)
    3. recordのvalue毎のパーサを生成する
    4. 今まで作ってきたものを合わせてrecord全体のパーサを生成する

まあだいたいこのまんまですが、3-3,3-4あたりがちょっと面倒くさかった。

何が面倒くさいって、Haskellでは値の蓄積とか逐次処理とかがそれなりに機能として分離されてるからrecordを構築するのが面倒くさくて、結局デフォルト値のrecordを用意して、そのrecordを状態として持つStateT Parserを使ってパースした値を蓄積する処理を繰り返すという形でなんとか頑張った。というかこれ自体は別に難しくないんですが、これTHを書きながら構築していったので、TH上で仕様変更するのが難しかった。
つまり要らんところで引っかかっていたということです。

THを書く時は必ず生成対象のHaskellコードを用意しておいて、それに向かってそのコードを生成するTHを書いていった方がいいです。あたりまえですけど。テストもあるとなお良い。TDDにまで落とし込めると更に良い。THのまま仕様変更とかは案外うまくいくけども余計に時間がかかるからオススメできない。
私のコードの中に「こういうのを作る」ってコメントがたくさんあるのはそういう感じです。

あと最初のうちはTHに慣れてなかったもんだからあんまりクォートを使ってなかったんですけど、THは基本的にクォートで書いて、一部分だけクォートの中で構文木を接合する形で書いたほうが圧倒的に楽だしわかりやすいですね。
このあたりは、なんかいろんな定義や式の構文木を見ていると、だんだんどこが部分木でどういうものが接合できるのかというのがわかるようになってきます。っていうかghciで確認しながらやれば一発です。
Haskellの構文もはっきりと把握できるようになるのでこういう作業は楽しい。

あと、やっぱり結構クリティカルな部分を触っているせいか、GHCがパニクったりBug reportを要求してきたりとかいうことがよくありました。
これなんかは現在進行形で発生しているんですが
#7092 (Spurious shadowing warnings for names generated with newName) – GHC

Linux版GHCだと、newNameで作ったのに名前が被ってるって文句言われるというのはバグっぽいらしいです。
これが一番目立つんですけど、他にも結構ありました。これ以外は再現条件はわかりませんが。

まとめ