Meta_conv による OCamlデータ型 と 樹状データ の相互変換自動生成
Web にアクセスするプログラムを書いていると良く JSON というデータを扱う ことがあります。JSON とは世間で何と言われているかわかりませんが OCaml では:
type t = | String of string | Number of float | Object of obj | Array of t list | Bool of bool | Null and obj = (string * t) list
簡単ですね。まあ S式に毛の生えたようなものです。 型の無い世界の人はこの簡単な型でもって、何でもかんでも表現しているみたい。 例えば、このデータは必ず整数リストだという仮定があっても JSON では本当に データが整数リストであるか、どうか、確かめなきゃいけない。大変ですねー。
じゃあ OCaml だと楽かというと、やっぱり大変です。 JSON のデータをパースして上の t の形にしても、 やっぱり整数リストだという仮定を確かめねばならない:
(* t -> int list *) let get_int_list = function | Array xs -> List.map (function Number f -> int_of_float f | _ -> assert false) xs | _ -> assert false
ここでは float から int への変換を int_of_float で誤魔化してますけど、 これも 3.14 のようなデータが JSON に乗っていれば本当はエラーにしなければなりません。 別に技術上難しいことではないですが、これを全てのデータに関して一々書くのは とても大変です。
OCaml のデータ型の定義から JSON への自然な変換、逆変換があればとても便利そうですね! 例えば、 int list という型式を与えれば上の様な get_int_list のような動作をする関数が 自動的に生成されて欲しい。
OCamlのデータ型 <=自動変換!!=> OCamlのJSON表現 <=OCamlのJSONライブラリ=> JSONテキスト 自然なデータ表現 一般的過ぎる ダサいけどみんな使ってるし…
便利そうなら作りましょう!!
既存の OCaml のデータ型と JSON の相互変換関数自動生成
おっと、作るその前に、既にそういうものがあるか、どうか。実はある。
- json_tc
- CamlP4 の type_conv フレームワークを使った OCaml の型定義からの変換関数の自動生成
- atdgen
- type_conv のような文法拡張ではなく、OCaml の型定義を受け取り変換関数を自動生成するコマンドラインツール
じゃあ、いいじゃん、これ使えば、てことなんですけど、 json_tc は超非力で作者自体もうほったらかして atdgen を使っているそうです。atdgen は…私はほとんど使ったことがないのですが type_conv を使った sexplib に慣れている私には気にかかります。type_conv だと型定義の後に with hogehoge って書くだけで型定義から関数を自動生成してくれるから、気が楽なんですよね。
私は type_conv 好きなので、type_conv の方向で作ってみました、それも JSON に限らず他の樹上データを扱えるようにしてみましたよ!! (まだ JSON しか試してないけどさ)
meta_conv: OCaml のデータ型と 樹上データ の相互変換関数自動生成
meta_conv は json_tc を超強力にして sexplib を一般化したような type_conv フレームワークです。meta_conv 自体は特定の樹状データ(JSON とか S式とか XML とか)を扱う関数はありません。特定の樹状データの基本的な変換関数群を meta_conv に与えることでいろんなデータ型に対応できるようになっています。例えば:
type strings = string list with conv(json)
と書くと string list のための JSON デコーダとエンコーダが:
val json_of_strings : strings -> Json.t val strings_of_json : Json.t -> (strings, Json.t) Result.t val strings_of_json_exn : Json.t -> strings
こんな風に生成されます。 strings_of_json と strings_of_json_exn の違いはエラー処理を Result モナドで扱っているか、例外を投げるか、どうか。
これが上手く動くためには Json というモジュールに Json.t という樹状データ型が 定義されていて、 Json_conv というモジュールに Json.t 基本的な構成要素の変換関数が 定義されている必要があります。(ここは流石にちょっと複雑なので触れません。)
もし msgpack-ocaml に対して、Pack というモジュールで msgpack の内部表現が Pack.t という型で定義されており、その変換が Pack_conv で与えられていれば、:
type strings = string list with conv(pack)
と書けば pack_of_strings とか strings_of_pack が生成される、という訳です。 (まだ polymorphic variant については実装してないので @mzp さんの msgpack-ocaml の Pack モジュールはそのまま使えませんけれども)
で、type_conv を使っているので複数変換したいデータ型を並べることもできます:
type foobar = Foo of int | Bar of float with conv(json), conv(pack), sexp
当然、sexplib と併用もできます。
ラベル名を変えてみたり、余剰フィールドを無視、保存とか
type とか、 val とか、 OCaml の予約語がオブジェクトに出てくるような JSON を OCaml にマップしたい時はどうしましょう? type t = { type : string; val : string } と書きたいけれども、 type や val は使えません、その時は特殊記法を使います:
type t = { type_ as "type" : string; val_ as "val" : string } with conv(json)
こう書くと、ターゲット樹上データではクォートに囲まれた文字列がフィールド名として 使われます。フィールド名が大文字だったりした時もこれが使えます。ヴァリアントでも使えますよ:
type t = Foo as "foo" of int | Bar as "bar" of float with conv(json)
JSON の仕様があまりキチッとしてない Web サービスからナーンと無く興味のあるデータだけ取ってきたいって事、ありますよね。知らないデータは取りあえず無視したい、そんな時は、:
type t (: Ignore_unknown_fields :) = { name : string; password : string; } with conv(json)
と書きます。 (: Ignore_unknown_fields :) が無いと他のフィールドが JSON に乗っているとデコードエラーになりますけど、あると余分なものは単に無視されます。
無視されるってのはちょっと。残りは残りで JSON の形で見てみたいってこともありますね。そういう時は、:
type t = { name : string; password : string; rest : Json.t mc_leftovers; } with conv(json)
と書きます。``mc_leftovers`` は特殊な名前。この型を持つフィールドがあれば、残り物はここに放り込まれます。とりあえずこの t で JSON を変換してみて、 rest の中身を見ながら t にフィールドを追加していく…といったことが可能です。
糞い Web サービスだとフィールドがあったり、無かったりしますよね。ほんとうざいんですが、そういう時は mc_option を使う:
type t = { name : string; password : string; real_name : string mc_option; rest : Json.t mc_leftovers; } with conv(json)
こう書いておくと、もし JSON 中に real_name というフィールドがあれば real_name = Some "hogehoge" になりますし、無ければ real_name = None になる、という訳です。
meta_conv は OPAM から入手できます
meta_conv は opam で入手できます。meta_conv の例として JSON ライブラリと その基礎変換も作ってみました。 JSON ライブラリは OCamltter のものを勝手ですが、 tiny_json 名付けて、これまた opam で入手可能です。 @yoshihiro503 さんありがとうございます。 この tiny_json 用の変換モジュールとして tiny_json_conv も opam に登録しておきました。
現在私はこの tiny_json, meta_conv, tiny_json_conv を使って bitbucket の API を 叩いてみたり tumblr を覗いてみたり、 OCamltter の twitter API を Json.t から より typeful な物に変更したりしています。兎に角 OCaml の型が決まればまず自然に変換が 作れるのが便利です。