BuckleScript (+ webpack)を使いNode.jsやブラウザ上でOCamlを動かす

[BuckleScript](https://github.com/bloomberg/bucklescript)（BS）は、JavaScriptコードをターゲットにするOCamlコンパイラ。コンパイルが高速で、読みやすく簡潔なJavaScriptを出力し、簡潔なFFI（OCamlとJSの相互接続）が特徴とのこと。読みやすく簡潔なJSという点でHaskellライクなAltJSであるPureScriptと近い系統といえる。

[公式マニュアル](http://bloomberg.github.io/bucklescript/Manual.html)の情報を読んで使ってみたが、実際に上手くいくまでに少し試行錯誤があったので、忘れないうちに覚え書き。JSビルドのベストプラクティスもOCamlのエコシステムも中途半端な知識しかないので、ツッコミ歓迎です。

# 全体の流れ

```
OCaml
    ---------[BuckleScriptコンパイラ]-----> 複数のJSファイル (CommonJSモジュール)
    ---[Webpack]--> バンドル・minifyされた単一ファイルのJS
```

動くサンプルは https://github.com/nebuta/bucklescript-build-example に置いた。

# 使ったもの

* npm
* BuckleScript
* Webpack

# 環境の準備

## BuckleScriptコンパイラ(bsc)のインストール
```
npm init -y
npm i -D bs-platform webpack
```
bs-platformのインストールは少し時間がかかる（手元のMacBook Air, 13-inch, Early 2011で3分くらい）。`node_modules/bs-platform`はサイズが320 MB程度になる。環境によっては（すでにOCamlが入っている場合など、既存のものと衝突する？）ここでハマるかもしれない。

# ソースコードの準備

何でも良いのだが、例として、`src`フォルダを作成してその中に以下の2つのファイルを作成する。

```ocaml:src/main.ml
(* Function from: http://stackoverflow.com/questions/243864/what-is-the-ocaml-idiom-equivalent-to-pythons-range-function *)
let (--) i j = 
    let rec aux n acc =
      if n < i then acc else aux (n-1) (n :: acc)
    in aux j [] ;;

let () =
    let primes = List.filter Prime.is_prime (2--50) in
    let result = String.concat ", " (List.map string_of_int primes) in
    print_endline "Finding prime numbers.";
    print_endline result;
```

```ocaml:src/prime.ml
let is_prime n =
    let max_d = int_of_float (floor (sqrt (float_of_int n))) in
    let rec loop d =
        if d > max_d then true
        else
            (n mod d <> 0) && loop (d+1)
    in loop 2;
```

```index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>BuckleScript test</title>
</head>
<body>
<script src='./dist/bundle.js'></script>
    
</body>
</html>
```

# ビルド

```shell-session
mkdir -p dist
./node_modules/bs-platform/bin/bsc -I src -c -bs-main src/main.ml -bs-package-name [任意のパッケージ名] -bs-package-output dist
./node_modules/webpack/bin/webpack.js -p dist/main.js dist/bundle.js
```

`bsc`がBSのコンパイラで、OCamlのコンパイラのコマンドオプションに加えてbsc独自のオプションがいくつかある。

* `-I`はソースコードのあるフォルダを指定。
* `-c`はコンパイルのみ行って実行しない。つけないとコンパイル後そのまま実行してしまう。
* `-bs-package-output`はコンパイル後のjsファイルを置くフォルダを指定。

コンパイルは一瞬（15ミリ秒程度）で終わる。生成されたjsファイルは以下の様になる。極めて簡潔かつ読みやすいコードになった。また、`is_prime`関数の末尾再帰最適化（ループへの変換）もされている。

なお今回は、`bsc`と`webpack`はプロジェクト内のnode_modulesのローカルなものを使っているが、`npm i -g`で入れたグローバルなものでもおそらく問題ない。

```dist/main.js
// Generated by BUCKLESCRIPT VERSION 1.0.1 , PLEASE EDIT WITH CARE
'use strict';

var Prime      = require("./prime");
var Pervasives = require("bs-platform/lib/js/pervasives");
var $$String   = require("bs-platform/lib/js/string");
var List       = require("bs-platform/lib/js/list");

function $neg$neg(i, j) {
  var _n = j;
  var _acc = /* [] */0;
  while(true) {
    var acc = _acc;
    var n = _n;
    if (n < i) {
      return acc;
    }
    else {
      _acc = /* :: */[
        n,
        acc
      ];
      _n = n - 1 | 0;
      continue ;
      
    }
  };
}

var primes = List.filter(Prime.is_prime)($neg$neg(2, 50));

var result = $$String.concat(", ", List.map(Pervasives.string_of_int, primes));

console.log("Finding prime numbers.");

console.log(result);

exports.$neg$neg = $neg$neg;
/* primes Not a pure module */
```

```dist/prime.js
// Generated by BUCKLESCRIPT VERSION 1.0.1 , PLEASE EDIT WITH CARE
'use strict';

var Caml_int32 = require("bs-platform/lib/js/caml_int32");

function is_prime(n) {
  var max_d = Math.floor(Math.sqrt(n)) | 0;
  var _d = 2;
  while(true) {
    var d = _d;
    if (d > max_d) {
      return /* true */1;
    }
    else if (Caml_int32.mod_(n, d) !== 0) {
      _d = d + 1 | 0;
      continue ;
      
    }
    else {
      return /* false */0;
    }
  };
}

exports.is_prime = is_prime;
/* No side effect */
```

# 実行

以下のいずれかで動いているのを確認できる。

* Node.jsで`node dist/main.js`を実行。
* Node.jsで`node dist/bundle.js`を実行。
* ブラウザで`index.html`を開く。`dist/bundle.js`が読み込まれて実行される。コンソールを開くと出力が見られる。

```shell-session
$ node dist/main.js 
Finding prime numbers.
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47
```

# ビルド後のファイルサイズ

Webpackでバンドル後（＝外部のJSコードに依存しない状態）のサイズだが、Hello Worldであれば250バイト程度（minify後）で、余計なランタイムなどない極めて小さいファイルになる。

ただし、OCamlの標準ライブラリ（`node_modules/bs-platform/lib/js`下にコンパイル済みのJSファイルが置いてある）など、他のモジュールを使うと、モジュールごとに丸々バンドルされるので、バンドルのサイズはその都度大きくなってしまう。たとえば、今回のコード例はPervasive（暗黙にインポートされるライブラリ。HaskellのPreludeのようなもの。）を使っているので、webpackでminify後に37 kBとなる。

この辺りは将来BSからES6モジュールを出力＋Rollupで不要なインポートを削除できるようになることを期待（現状ではBSはES6出力に未対応 https://github.com/bloomberg/bucklescript/issues/681）。

# まとめ

OCamlの機能をフルに活用できる上に、コンパイルが異常に速く、生成するコードも小さいということから、BSはAltJSとしてかなり有望な選択肢になると思われる。

# 今後調べたいこと

* FFI
  * 今調査中なので、また別の機会に記す予定。FFIの使いやすさはAltJSの使いやすさそのものに非常に大きな影響を与えるので、BSではどのくらい使いやすいのか気になるところ。

* OPAMから入れたパッケージの使い方
  * そもそも使えるのかどうか。