ぼちぼち日記

おそらくプロトコルネタを書いていることが多いんじゃないかと思います。

Node-v0.12からデフォルトでES6の一部が使えるようになった(WeakMap解説編)

このエントリーは、4月24日(木)に行われる「東京Node学園 12時限目」の発表ネタを先出ししたものです。

1. はじめに、

 過去2回、Node-v0.12における ES6(ECMAScript6)関連のエントリー*1を続けて書きました

いつNodeでES6の機能がデフォルトで使えるようになるのか…と夢見てたのですが、なんとすぐその時がやってきました。

パチパチ! Angular2.0 の開発のため Chrome M35 の機能として先行的に一部ES6対応したようです。 *2

Googleさん、えらい。

 Nodeの現リーダTJも、最初はこのV8アップグレードに”ちょっと待った”をしていたみたいですが、今では”現状容認”に変わりました。
 今後、V8 3.25で何か問題があればダウングレードする可能性がありますが、このままいけば Node-v0.12 で Promise, Object.observe, WeakMap,WeakSet がデフォルト(オプションなし)で使えるようになりそうです。 既にPromise, Object.observe の解説エントリーを書いたので、今回残っている WeakCollection (WeakMap/WeakSet)について書いてみます。

2. WeakMap/WeakSet とは、

 オブジェクトのみをキーにしたハッシュテーブルです。WeakMapは、キーと値を WeakSet はキーのみ登録できます。弱参照なのでキーがGCされるとエントリーが自動的に削除されてしまいます。そのため for とかで内部のエントリーを列挙したり、格納されているエントリー数を取得するようなことはできません。
 今回、WeakMapの方がいろいろ使い方があるので WeakMapだけ解説します。まず単純な使い方を見てみましょう。

// sample0.js WeakMapの使い方
// WeakMapコンストラクタで weakmapオブジェクトを生成
var weakmap = new WeakMap();
function MyObj(i) {
  this.hoge = i;
}
var obj1 = new MyObj(1);
weakmap.set(obj1, 1); // obj1をキーに1を格納
var obj2 = new MyObj(2);
weakmap.set(obj2, 2); // obj1をキーに1を格納
// WeakMap値を取得
console.log(weakmap.get(obj1));
console.log(weakmap.get(obj2));
weakmap.clear(); // エントリーをクリア
console.log(weakmap.get(obj1));
console.log(weakmap.get(obj2));

で、実行してみます。

$ node -v
v0.11.13-pre
$ node sample0.js
1
2
undefined
undefine

キーのオブジェクトがないと中身がわからないので、GCでエントリーが削除されているか確認する方法が思いつかないです。もし誰かやり方を知っていたら教えてください。
 このWeakMapは、もともと soft field と object cache という目的で仕様化された機能です。ブラウザでDOMを扱う場合、DOMのプロパティを勝手に拡張してフィールドを付け加えるのではなく、WeakMapを使いましょうということです。Cacheの用途は、エントリー削除ポリシーとかの制御ができないのでなかなか限定的な使い方になりそうです。(あまりいい例が見つからなかった)

3. WeakMapの使い方(soft field)

 Nodeの場合でも、王道の soft field での使い方を考えてみます。HTTPリクエストのレスポンス時間の統計を測るスクリプトを考えてみます。クライアントオブジェクトの開始時間の格納にWeakMapが使えます。*3

// sample3.js HTTPリクエストのレスポンス時間統計を測定
var http = require('http');
var opts = {
  host: 'localhost',
  port: 80,
  path: '/'
};
var maxi = 100; // 試行回数
var results = [];
var weakmap = new WeakMap(); // WeakMapを生成
for(var i = 0; i < maxi; i++) {
  var client = http.get(opts);
  // リクエストの開始時間を weakmap に保存
  weakmap.set(client, Date.now());
  client.on('response', function(res) {
    res.resume();
    res.on('end', function() {
      // 開始時間からレスポンス時間を計算して結果を格納
      results.push(Date.now() - weakmap.get(client));
    });
  });
}
// 統計結果出力
process.on('exit', function() {
  var max = 0;
  var min = Infinity;
  var ave = 0;
  results.forEach(function(e) {
    max = e > max ? e: max;
    min = e < min ? e: min;
    ave += e;
  });
  if (!results.length) ave = ave/results.length;
  console.log('min:', min, ',max:', max, ',average:', ave, 'in [msec]');
});

で実行してみます。

$ node sample3.js
min: 30 ,max: 69 ,average: 52.76 in [msec]

まさに WeakMapの王道の使い方です。

4. WeakMap の使い方(プライベート変数の格納)

 他にユースケースないかなーと調べてみたんですが、なかなか皆さん苦労されているみたいです。一つこれは、と見つけたのは、Nodeの用途に限ったものではないですが、WeakMapを使ってプライベート変数を隠匿させる方法です。

 ご存知の通り JavaScript では、プライベート変数の機能が標準で提供されていません。例えば、乱数値を格納するクラスを考えると、アンダースコアを変数名に付けて、下記の通りなんちゃってプライベートにしていることも多いです。

module.exports = Hoge;
function Hoge(size) {
  size = +size || 8;
  // 変数をアンダースコア名にしてプライベート感を演出
  this._rand = require('crypto').randomBytes(size);
};
Hoge.prototype.getRand = function() {
  return this._rand;
};

他に クロージャー内の変数を使うと下記の通り変数を隠匿できますが、アクセスするにはコンストラクタ内のスコープじゃないとダメなので、見栄えがちょっとイケてないですね。

module.exports = Hoge;
function Hoge(size) {
  size = +size || 8;
  var rand = require('crypto').randomBytes(size);
  this.getRand = function() {
    return rand;
  };
};

そこでWeakMapを使うとちょっときれいに書けます。モジュール内に WeakMapを生成して、this をキーとしてプライベート変数を格納するのです。
早速書いてみると、

// WeakMapを使った Private変数の隠匿
var weakmap = new WeakMap();
module.exports = Hoge;
function Hoge(size) {
  size = +size || 8;
  var rand = require('crypto').randomBytes(size);
  // thisをキーとしてプライベート変数をWeakMapに格納
  weakmap.set(this, rand);
};
Hoge.prototype.getRand = function() {
  // WeakMapからプライベート変数を取り出す
  return weakmap.get(this);
};

これ結構良い使い方ですね。FirefoxOS アプリのソースとか見ると WeakMapの定番の使い方みたいになってる感じです。
 先のエントリーで、クロージャー変数を使う方法よりWeakMapの方が prototype オブジェクトを使えて、メモリ的に節約できるメリットがあると書いてあったので、実際に比べてみました。10万個のオブジェクトを生成してみます。

var kind = process.argv[2] || 'weakmap';
var Hoge = require('./' + kind + '-module.js');
var hogelist = [];
for(var i = 0;  i < 100000; i++) {
  hogelist.push(new Hoge(16));
}
console.log(kind, process.memoryUsage());

で実行します。

$ node sample2.js weakmap
weakmap { rss: 52260864, heapTotal: 35416704, heapUsed: 20998084 }
$ node sample2.js closure
closure { rss: 49442816, heapTotal: 35178496, heapUsed: 24271728 }

うーん、100,000オブジェクトを作って2割程度の違いですか。おそらくV8の hidden class が効いているのでメモリや性能的にはそれほど違いはないかと推測します。もし、他にもWeakMapの活用方法を知っている方がいらっしゃいましたら紹介してください。

5. Node-v0.12の新機能について発表します

「東京Node学園 12時限目」は、来週月曜4月21日に募集開始です。この他 Node-v0.12 の新機能についていろいろ発表する予定です(発表予定アジェンダ)。 20分ではなかなか細かく全部は説明できませんが、新しいNodeがどういうものになるのか知りたい方は是非ご参加ください。

*1:厳密には Object.observeはES7の機能です。

*2:不思議なんですが現breeding edge V8 3.26では再度オプション要に revert されています。誰か理由を知っている人は教えてください。

*3:実際は、そのままクライアントオブジェクトにプロパティを追加してもまぁ問題ないでしょう