20
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-09-10

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つのファイルを作成する。

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;
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>

ビルド

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関数の末尾再帰最適化(ループへの変換)もされている。

なお今回は、bscwebpackはプロジェクト内の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が読み込まれて実行される。コンソールを開くと出力が見られる。
$ 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から入れたパッケージの使い方

    • そもそも使えるのかどうか。
20
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?