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予約語がオブジェクトに出てくるような JSONOCaml にマップしたい時はどうしましょう? 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 APIJson.t から より typeful な物に変更したりしています。兎に角 OCaml の型が決まればまず自然に変換が 作れるのが便利です。

えっ、OPAMの使い方自体とか、pa_*.cmo の使い方がわからないですって?

それぐらい自分で調べなさい