BuckleScript(BS)は、JavaScriptコードをターゲットにするOCamlコンパイラ。コンパイルが高速で、読みやすく簡潔なJavaScriptを出力し、簡潔なFFI(OCamlとJSの相互接続)が特徴とのこと。読みやすく簡潔なJSという点でHaskellライクなAltJSであるPureScriptと近い系統といえる。
公式マニュアルの情報を読んで使ってみたが、実際に上手くいくまでに少し試行錯誤があったので、忘れないうちに覚え書き。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つのファイルを作成する。
(* 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;
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;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BuckleScript test</title>
</head>
<body>
<script src='./dist/bundle.js'></script>
</body>
</html>
ビルド
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
で入れたグローバルなものでもおそらく問題ない。
// 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 */
// 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
が読み込まれて実行される。コンソールを開くと出力が見られる。
$ 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から入れたパッケージの使い方
- そもそも使えるのかどうか。