λx. x K S K @はてな

このブログ内に記載された文章およびコードの著作権は,すべて Keisuke Nakano に帰属します.

#006 禁断の Obj

先日,フランスの XML 関係の研究者と共著する機会があり, 「こっちの方が早いだろ」「いやココを変えればこっちの方が」 「いやいやまだまだ改善できる」だの OCaml のコードで文通をしたりしていたが, そんな中,相手のこの一行に愕然としてしまった.

external set_field : 'a -> int -> 'b -> unit = "%obj_set_field"
確かに,効率的な実装を実現するうえで Obj モジュールを使うことはたまにあるが, ここまでやってしまうとは. というわけで,今日は Obj モジュールについて少しだけ触れる. ただし,この記事は Obj モジュールの利用を推奨するものではない.むしろ禁止したい. また,その性質からバージョンによって動作しない可能性も高い. ちなみに,以下のプログラムは 3.09.2 での動作を確認している.

「Not for the casual user.」とマニュアルにもある通り,Obj モジュールを使うことは推奨されていない. Obj モジュールは OCaml が扱う全てのデータ構造をいじる関数を提供しており, このモジュールを使うと関数型っぽくない,いわゆる『汚い』プログラムがいくらでも書けてしまう. 型を任意に変換できる関数 Obj.magic : 'a -> 'b を代表としてよく引き合いに出されるが, ここでは他の関数についても採りあげることにする.

Obj モジュールの扱う Obj.t 型は, OCaml が扱う全ての値(関数も含む)を表現できる構造体のようなものを表す型である. OCaml における任意の値は Obj.repr によって Obj.t 型に変換される. 型は変換されているが,恒等関数なので実際には何もしていない. 逆に,Obj.t 型を元の型に戻すのが Obj.obj である. ただし,文脈によっては型を明示する必要があるかも知れない. つまり,Obj.magicfun x -> Obj.obj (Obj.repr x) と等価である. (結局どれも恒等関数なのだが…)

OCaml で扱われる値は実装上はタグといくつかのフィールドから成り, それぞれ Obj.tag : Obj.t -> intObj.field : Obj.t -> int -> Obj.t で取得できる. Obj.field の二つ目の引数は 0 から始まるフィールド番号である. 例えば,リストは先頭と残りのリストの二つのフィールドを持っているので,

(Obj.obj (Obj.field (Obj.repr [1;2;3]) 1) : int list)
は,[2;3] を返す. また,Obj.set_field によって,あるフィールドの値を変更することも可能である. OCaml では,公にはレコードのみ mutable なフィールドを許しているが, Obj.set_field を使うとレコード以外でもフィールドの更新が可能になる. このため,mutable なヴァリアントを作りたい場合でも引数をわざわざレコードにする必要がなくなり,少しだけ効率が良くなることもある. ちなみに,冒頭の関数 set_field は,fun x i y -> Obj.set_field (Obj.repr x) i (Obj.repr y) と等価である. 無駄な関数呼び出しを減らすために external 宣言したと思われるが, 余程の理由がない限りあまり推奨しない.(コード自体はすっきりするかもしれないが…)

ここで,Obj を使った簡単なプログラム例を一つ示す. OCaml ではリング状のデータを let rec ring = 1::2::3::ring などとリスト型のデータとして作ることが可能であるが, let rec の文法の制約上, 与えられた任意のリストをリング状に変更するような関数を記述することはできない.そこで,Obj を使うと以下のように書ける.

let list2ring = function
    [] -> failwith "list2ring"
  | _::rest as top ->
    let rec loop prev = function
        [] -> Obj.set_field (Obj.repr prev) 1 (Obj.repr top)
      | _::xs as cur -> loop cur xs in
        loop top rest; top
ただ,この例に関しては Obj に頼らず Camlp4 を使う方が安全である.

また,Obj を使った簡単な実験から OCaml の実装を垣間見ることができる. 例えば,現行の OCaml の内部では文字列とフォーマットを区別していないので,

let str = "%%%%%%" in
str.[1] <- 'd'; str.[5] <- 's';
Printf.printf (Obj.magic str) 80 "zenkai!"
は,「80%zenkai!」と出力する.

最後に,しつこいかも知れないが Obj を使うことは絶対に推奨しない. どうしても使う場合は自己責任で.