type holyshared = Engineer<mixed>

PHP、Hack、Ruby、OCaml、Rust、Javascript周りの技術ブログ

js_of_ocamlでモジュールの実装試した

js_of_ocamlで適当なモジュールを実装してみたので、その方法を書いておきます。
モジュールの実装はFFIなどのバインディング実装のような感じで、JavaScriptの型とOCamlの型を意識して実装する必要がありました。

モジュールで提供する関数の引数、戻り値などはJavaScriptの型で書く必要がありました。
OCamlの型のまま実装すると、JavaScriptのデータが期待したものに変換されません。
コード自体はコンパイルできてしまうので、mliファイルを書かずに実装すると問題に気づきにくいです。

今回は次の順番で解説します。

  1. JavaScript型の表現 - これがわからないとモジュールのインターフェース表現できない...
  2. モジュールの定義 - 型の表現がわかると、mliファイルがかける
  3. モジュールの実装 - mliファイルがあるので、仕様どうり実装する

JavaScriptの型表現

JavaScriptからの引数、戻り値は基本的にOCamlの既存の型を使用できません。
なので、js_of_ocamlで用意されている型表現を使用します。  

例えば、OCamlで文字列を受け取って、文字列を返す関数は下記のような記述になります。

val name: string -> string

しかし、js_of_ocamlバイトコードJavaScriptに変換する場合は下記のようにする必要があります。

val name: Js.js_string Js.t -> Js.js_string Js.t

JavaScriptの型を表現するには 'a Js.t を使用します。
型パラメータ 'a には具体的な型を指定します。

代表的な型は下記の通りです。

表記
Number Js.number Js.t
String Js.js_string Js.t
Array ('a Js.js_array) Js.t

例えば、JavaScriptのNumber型の配列表現は ((Js.number Js.t) Js.js_array) Js.t になります。
()を省略して Js.number Js.t Js.js_array Js.t と書いても同じ意味になります。

モジュールの定義

型表現がある程度わかったら、モジュールのインターフェースを記述することができます。
外部に公開するモジュールで下記のようなモジュールのインターフェースがあるとします。
これは num_op というオブジェクトがあり、 increment というメソッドを実装しているという意味になります。

increment は Number型の配列を受け取り、1を足して返すメソッドです。

(* num_op.mli *)
open Js_of_ocaml

val num_op: <
  increment : ((Js.number Js.t) Js.js_array) Js.t
    -> (((Js.number Js.t) Js.js_array) Js.t) Js.meth;
  > Js.t

モジュールの実装

increment は Number型の配列を受け取り、1を足して返すメソッドなので、 Js.array_map を使えば良さそうです。

Js.array_map の仕様は ('a -> 'b) -> 'a Js.js_array Js.t -> 'b Js.js_array Js.t です。
つまり、配列の要素に適用する関数を第一引数に受け取り、第二引数に対象の配列を指定します。

配列の要素はJavaScriptのNumber型なので、このままだと計算できません。
なので Js.float_of_number を使用して、OCamlのfloat型 に変換してから計算します。

また、計算結果は OCamlのfloat型 ではなく、JavaScriptのNumber型 で返す必要があります。
ここを間違えると、バイトコードから生成したJavaScriptが期待した動作をしません。

実装は下記の通りになります。

(* num_op.ml *)
open Js_of_ocaml

let num_op = (object%js
  method increment nums =
    let add_one n = (Js.float_of_number n) +. 1.0
      |> Js.number_of_float in
    Js.array_map add_one nums
end)

let _ =
  Js.export "num_op" num_op

モジュールのコンパイル

js_of_ocamlバイトコードからJavaScriptに変換するので、 ocamlc を指定してコンパイルします。

ocamlfind ocamlc -package js_of_ocaml -package js_of_ocaml-ppx -linkpkg \
  -o num_op.byte num_op.ml

次にバイトコードから、JavsScriptへ変換します。
--no-runtime を指定しない場合、ランタイムのコードも含まれます。

js_of_ocaml num_op.byte

JavaScriptの動作確認

適当なHTMLを用意して、コンソールにログを出力してみます。
ログに結果が表示されていればOKです。

<!doctype>
<html>
  <head>
    <title>example</title>
    <script src="num_op.js"></script>
    <script>
      console.log(num_op.increment([1, 2]));
    </script>
  </head>
  <body>
  </body>
</html>

まとめ

  • JavaScriptのライブラリをOCamlで書くのは辛い、全てOCamlで完結するならいい。
  • コンパイルが通って、JavaScriptが出力されても安心できないので、出力されたJavaScriptのテストコードがいる。
    • そこまでやるならFlow、TypeScriptの方がまだアドバンテージありそうな気がする。
  • コンパイル早いのは嬉しい。
  • JavaScript目当てなら、ReasonMLの方がいいかもしれない。

成果物はgistに置いてあります。 https://gist.github.com/holyshared/3233eb6e5474a0e226ce4216dff0fcd1