XML をなんとなく OCaml のデータに落とすお手軽な方法をお手軽に解説する
うん、何かちょうどこういうやっつけ仕事をしていたらセックスがピー (http://twitter.com/chunjp/statuses/6523318056) とかいうのが多方面から聞こえてきたんよ。それで。若い人はなかなかそういうの恥ずかしくて話せないでしょ、だからアラフォーのおっちゃんがちょっとしたるわ。
目的
何か XML で書かれたデータがあって、何となくどういうデータ構造になってるかは理解できる。あー、ここはリストだな、とか、ここは OCaml でいうレコードっぽいとか。そういうデータを読み込んで OCaml の型に落とし込むという作業、これをできるだけお手軽にしたい。スキーマとかガン無視。そういうのでやりたい人はがんばってください。できるだけお手軽にが基本。
材料
xml-light でパース
はい、これで OCaml のデータになりました。でもこれじゃただの untyped な木だ(Xml.xml としては typed data structure ですけど、もちろん)。使えねー。
ここで HaXml かと思ったが
HaXml (http://www.cs.york.ac.uk/fp/HaXml/) は Haskell で書かれた XML フィルタコンビネータライブラリ。まーいろんなことができる。論文があるけど別に理論とか扱った論文じゃないので簡単に読める。でそれを参考にちょちょっとやれば OCaml でも同じ様なことができる。私も一度やったことがありますが、今回はダイレクトにはやりません。
そのまま Sexp にする
どうせ、木なんだから、別の木に変えても誰も困らない。どうせ untyped だし。
<hoge key1="value1" key2="value2">blah blah</hoge>
を
((tag "hoge") (attrs ( (key1 value1) (key2 value2) )) (contents "blah blah"))
に対応する Sexplib のデータに変換する。変換関数は自明ね。
ここで HaXml for Sexp をかます
Sexp は XML を単純にした木のデータだから同じようなコンビネータを同じように書けて、さらに単純だから綺麗。HaXml というか、HaSexp。HaXml の XML 特有のたとえばアトリビュートをいじったりする部分とかが、もっと一般的な Sexp をいじるコンビネータの組み合わせで書ける。
もちろん、XML のデータ構造を Sexp に落とし込むと生成される Sexp には一定のインバリアントがあって、それを無視しちゃうコンビネータの使い方をするとバグったプログラムを書ける可能性が増える。たとえば、ボヤッとしてると attrs の中に (tag s) があったときそれを XML のアトリビュート tag="s" じゃなくて、タグ名 と誤解するプログラムを書くことができちゃう、けど、お手軽が第一義なので、気にしない。
HaSexp のソース?ねーよ。俺に必要なコンビネータしか書いてないし。論文読めば 30 分で書けるよ。
ML フレンドリな名前に変える
XML データには自分の OCaml プログラムにとって興味のないデータとか、そのままでは OCaml フレンドリではないタグやアトリビュート名があるので、それを上の HaSexp でフィルターするなり変換するなりしてやる。で、目標として XML のデータを落とし込みたい OCaml のデータ型の Sexplib での表現形として解釈できるとこまで持っていく。
Sexplib で Sexp から OCaml のデータに変換する
OCaml のデータ型の値として XML を解釈してやるために Sexp に変換する下準備をやってきた。後は Sexplib にその Sexp を投げて好みのデータ型に変換してもらうだけ。
Sexplib には CamlP4 モジュールがついていて、typeconv というのと組み合わせて使うと、OCaml データ型に対応する Sexp の形を自動的に作って、OCaml <=> Sexp の変換関数を勝手に作ってくれるというナイスな機能がある。これが Sexplib の一番の目玉だよケン坊(http://d.hatena.ne.jp/komamitsu/20091211/1260544597)。
じゃあたとえば、株のコード、シンボルのデータ型を書いてみる。名前と、ロイターズとブルームバーグのコードの三つ組みってことにしとこう。
TYPE_CONV_PATH "Symbol" (* おまじない *) type symbol = { name : string; (* human readable name (ex. Nintendo) *) reuters : Reuters.t; (* reuters code, ex. 7974.T *) bloomberg : Bloomberg.t; (* bloomberg code, 7974.JP *) } with sexp
Reuters.t と Bloomberg.t の構造は、省略。最後に with sexp と書いてやる。もちろんこれは OCaml の文法じゃない。TYPE_CONV_PATH もおまじないとして必要。Sexplib のパーサ拡張 pa_sexp_conv と pa_type_conv を使ってこのプログラムを CamlP4 に通してやると、大体こんな感じになる:
type symbol = { name : string (* .. *) reuters : Reuters.t; bloomberg : Bloomberg.t; } (* Sexp の型は Sexp.sexp だっけか?忘れた、わかるやろ *) let sexp_of_symbol : symbol -> Sexp.sexp = fun v -> ... let symbol_of_sexp : Sexp.sexp -> symbol = fun v -> ...
この関数を使って OCaml 界と Sexp 界の橋渡しをしてやればいい。Sexp の形が symbol_of_sexp が期待している物と違った場合はちゃんとエラーになる。
型 symbol の中には Reuters.t と Bloomberg.t というデータ型があるが、その変換関数が存在している必要がある。これも Sexplib の with sexp を使うか、それとも自分で書いてもいい:
module Reuters : sig type t = ... (* 知らんがな *) val sexp_of_t : t -> Sexp.sexp val t_of_sexp : Sexp.sexp -> t end = struct type t = ... (* 知らんがな *) with sexp (* 自動生成やっちゅーねん *) end module Bloomberg : sig type t = ... (* 知らんがな *) val sexp_of_t : t -> Sexp.sexp val t_of_sexp : Sexp.sexp -> t end = struct type t = ... (* 知らんがな *) let sexp_of_t = ... (* 自作してみた *) let t_of_sexp = ... (* 自作してみた *) end
作業は XML 由来の Sexp の形を HaSexp で変形して Sexplib で解釈可能な形にする、というのに帰着
この変換関数に適合する Sexp を XML からなんとなーく作ってあげるために HaSexp を使って変換をだらだらーと手動で書く。いらないフィールドを消したり、OCaml で不正なラベル名になるアトリビュートキーを直したり。はじめは面倒だがすぐ繰り返し作業になっているのに気づいてある程度一般的なライブラリみたいなのができるよ。
まー問題は Sexplib で OCaml のデータ型がどういうフォーマットの Sexp になるか、ある程度理解してないとこの手法は使えんってことですわ。でもそんな難しいものでもないので XML => OCaml で時間を浪費している人はチャレンジする価値はあると思います。
with sexp を改造してクールに with xml とかやってもいいのかもしれないけど、どのタグをどう解釈するか一意でないので、OCaml の型定義に多分それこそ大量のアノテーションをつけてやってそれを P4 ですげー賢く解釈してやるとか、それかそれこそスキーマを読み込んでそこから OCaml のデータ型を何とか自動で作ってやるとかになりそうですが、それはこの上なく手間が懸かる上に上手くいかないに決まっている。と思います。つーかそんなことにチャレンジしてたら本題の XML を読み込む仕事がいつになってもできませんがな。