js_of_ocaml で enchant.js (試行錯誤編)

転職活動を始めたら, 色々考えることができて妙に忙しく感じてしまっています. 話を聞くともっとすごい人はざらだから, 自分は温い方なんでしょうが.
js_of_ocaml を触ってみたのも, 転職活動に際してのことです. もちろん前から興味はありましたが, 転職の方向性から, いくらなんでも JavaScript を触らないとならんだろう, という感じになったからです. そこで TypeScript とか Haxe とかを選ばないあたりがなんというか.

それはそうとして, js_of_ocaml で enchant.js の一つめのサンプルを移植できたのはよかったのですが (gist を初めて使ってみました. ↓です).

次のシューティングのサンプルのところで完全に躓いてしまいました. 躓いた原因ははっきりしていて, サンプルの以下の部分です.

var Player = enchant.Class.create(enchant.Sprite, {
initialize: function(x, y){
enchant.Sprite.call(this, 16, 16);
this.image = game.assets['graphic.png'];
this.x = x; this.y = y; this.frame = 0;
game.rootScene.addEventListener('touchstart', function(e){ player.y = e.y; game.touched = true; });
game.rootScene.addEventListener('touchend', function(e){ player.y = e.y; game.touched = false; });
game.rootScene.addEventListener('touchmove', function(e){ player.y = e.y; });
this.addEventListener('enterframe', function(){
if(game.touched && game.frame % 3 == 0){ var s = new PlayerShoot(this.x, this.y); }
});
game.rootScene.addChild(this);
}
});

http://enchantjs.com/ja/sample.html

JavaScript は昔ちらっと触ったくらい (prototype.js がもてはやされて, まだ jQuery が無かったくらいの時代です, 確か) で, それから JavaScript 界隈にはノータッチでしたが, 意味的にはわかりやすく, enchant.Sprite を継承して, 第二引数のハッシュを継承したクラスに付与する, という感じでしょう. この場合, enchant.Sprite にも initialize メソッドはあるみたいなので, 初期化メソッドのオーバーライドがやりたいこと, といったところでしょうか?
さて、JavaScriptはprototypeベースという変わったクラス機構のおかげで、異常なほど柔軟なオブジェクトの生成とかができるみたいですが、その柔軟すぎる点が痛いです。(こういう柔軟さがうらやましくなるのは、スタブが欲しいけど作るのがめんどい、という時です。私あまりテスト書かないんですけど。)

さて、これをjs_of_ocamlにて翻訳してみようとすると、私的には次の点がものすごい気になりました。気になったというかどうすりゃいいの?となったというか。

  • initialize メソッドの中でthis使ってるけど、js_of_ocamlでthis使えんの?
  • ハッシュの中に関数を入れる方法が・・・。Js.variableでinitializeをまるごと書くしかない?
  • このメソッドの中で使ってるgame,どっから来たの・・・

という点がなかなかどうしようもなさそうでした。最後の点は、私がJavaScript全然理解していないせいだとは思いますが、私の頭じゃどう考えてもスコープ上に無いものを突然呼んでるようにしか見えないんです。謎すぎます。ちなみに上記のコードの前には、enchant();しかないのでクロージャでもないようなのでさらに謎なのです。
まぁそんなことは置いておいて、解決策を試行錯誤してみました。

動的なクラスをjs_of_ocamlで?

js_of_ocamlで提供されているClass typeでJavaScriptのオブジェクトをエンコードする方法は、「エンコードはするけど実装は知らんよ?」というものです。Class typeでやるので、当然そのクラスにはシグネチャ(この場合はサブタイプなんでしょうか?)しかありません。
シグネチャしかなくても、継承関係を表すこと自体は、inheritを使えばできます。実際、上ではすでに用意してあるSpriteをエンコードしたClass typeをinheritしてやればとりあえず問題ありません。

ここで中々思いつかなかったのが、「Unsafeを使わずにハッシュを渡す方法」でした。どうも今のjs_of_ocamlでは、ハッシュについてはUnsafe.variableで作るしかなさそうですが、追加するインターフェースについては、ちょっと考えればなんとかなりそうな方法で追加できました。
上のソースを単純にjs_of_ocamlに移植してみたものです。一部分だけですので、これだけでは動きませんが。

class type _player_extend = object
  method initialize: (_game t -> int -> int -> unit) prop
end

class type _player = object
  inherit _sprite
  inherit _player_extend
  method _Player: (_game t -> int -> int -> _player t) constr readonly_prop
end

class type _player_shoot = object
  inherit _sprite
  method _PlayerShoot: (int -> int -> _player_shoot t) constr readonly_prop
end

let player_shoot_class: _player_shoot t = Unsafe.variable "enchant.Class.create (enchant.Sprite)"

let _Player : (_game t -> int -> int -> _player t) constr =
  let initialize (game:_game t) x y =
    let this = Unsafe.variable "this" in
    ignore (Unsafe.meth_call enchant##_Sprite "call"
              [| Unsafe.inject this;
                 Unsafe.inject 16; Unsafe.inject 16
              |]);
    let assets = Unsafe.get game (Js.string "assets") in
    this##image <- Unsafe.get assets (Js.string "graphic.png");
    this##x <- x;
    this##y <- y;
    let touchstart (e:_event t) =
      this##y <- e##y;
      game##touched <- _true in
    let touchend (e:_event t) =
      this##y <- e##y;
      game##touched <- _false in
    game##rootScene##addEventListener (Js.string "touchstart", touchstart);
    game##rootScene##addEventListener (Js.string "touchend", touchend);
    game##rootScene##addEventListener (Js.string "touchmove", (fun e -> this##y <- e##y));
    this##addEventListener (Js.string "enterframe", (fun _ ->
      if (Js.to_bool game##touched) && game##frame mod 3 = 0 then
        ignore (jsnew (player_shoot_class##_PlayerShoot) (this##x, this##y))
    ));
    game##rootScene##addChild (this)
  in
  let extender : _player_extend t = Unsafe.variable "{}" in
  extender##initialize <- initialize;
  enchant##_Class##create (enchant##_Sprite, extender)

とりあえず途中まで書いてみているもの(動作します)は、gistに置いてあります。

何をしているのかというと、まずPlayerオブジェクトに追加/上書きするメソッドとかを、_player_extendsとして宣言します。で、これを_player(PlayerのClass type)にinheritで追加します。ここが一番意味わかんない場所ですが、let _Player : (_game t -> int -> int -> _player t) constrは、「コンストラクタ」です。
enchant.Class.createメソッドは、あくまで新しく作られたオブジェクトを返します。で、JavaScriptではオブジェクトのコンストラクタは、var Playerとかの変数名をそのまま使います。ですが、普通にplayerとかで受け取って、jsnew (player##_Player) (ほにゃらら)とかやっても、playerというオブジェクトの中にはPlayerという関数なんてねぇ!と怒られます。
そういうことで、返された値をコンストラクタとして保持しておくことで、とりあえずオブジェクトとして使えるようになる、という感じです。また、コンストラクタにgameをつっこむようにしたので、とりあえずinitializeの中でも使えるようにしました。まぁこの辺は仕方無いですね。

ですがこの手法には致命的な面があり、こうやって作った場合、当然ですがこの_Playerに束縛されているのはただのコンストラクタであり、オブジェクトとして使おうとするともれなく型エラーになります。当然ですが。
これを避けるためには、こういう動的に作成したクラスを格納するClass typeを作って、それにUnsafe.variable "{}"でとりあえず実体を与えておいて、その中に_Playerとかを格納する・・・というやりかたがありそうです。試してませんが。

thisの取得方法

↑で書いたソースにすでに書いてありますが、thisを取得する方法がどうにもなさげだったので、仕方なくUnsafe.variable "this"で取ってきています。ちゃんと動くので別にいいんですが、なんとも釈然としません。

スーパークラスのコンストラクタの呼び出し

もうひとつ問題となったのが、スーパークラスのコンストラクタ、今回はSpriteを呼ぶ方法ですが、元のサンプルではFunction.callを使って呼んでいます・・・が、この関数はよりによって可変長引数を持っている関数です。可変長引数のやつを呼ぶ方法はどうにもなさそうなので、これはあきらめてUnsafe.meth_callを呼んでいます。

まとめ

js_of_ocamlでprototypeベースのクラス弄りは辛い、と思いました。これがプログラム中で任意の文字列とかで呼ばれていたら、もうどうしようも無いような気がします。prototypeを弄るとか、そういう言語機能の差異はJavaScript側で隠蔽しておいて、js_of_ocamlではライブラリを型検査ありで使えるようにする、というのが一般的な利用方法みたいです。
それはそうとOCamlのclassってjs_of_ocamlを使うまで特に触ってなかったんですが、サブタイプという概念がなんとなく気持ちいいです。この部分だけRubyとかみたいです。でも型がきっちりしてるのはOCamlです。

まー、enchant.jsのように、JavaScriptの言語機能をフルで利用しているようなものを、js_of_ocamlに移植するのはかなり辛そうです。元々クラスベースであるHaxeとかでは、結構綺麗に実装されているのを見ましたが、根本的に違うOCamlでは中々・・・。
もし何かいい情報をご存知の方がおられましたら教えていただければありがたいです。英語ならなんとか読みます。