Block Rockin’ Codes

back with another one of those block rockin' codes

「for やめろ」またはイベントループと nextTick()

ものすごく遅レスですが、LLDiver で @ さんの LT であった話。

forやめろ、あるいは「繰り返し」という呪縛から逃れるために

簡単に言うと、 1~10 までを出力する方法を複数考えるというもの。 for, while, 再帰, goto etc.. と出て、途中で終わっちゃったので結論はよくわかりませんでしたが、 Node ではどれも使わずにできるな、と思ったのでちょっと例を出してみます。

ちなみに、タイトルでネタバレしている通りイベントループの話です。 そしてよくある「イベントループとは何か」「なぜ止めてはいけないのか」「process.nextTick() とは何か」「setImmediate() と何が違うのか」 などを解説する良い例だったので、書いてるうちに実はそっちがメインの解説となりました。

サンプルの実行結果は Node v0.11.13 です。(書き終わってから stream の古いインタフェースだったことに気づいた、、ごめんなさい)

リソースの読み込み

例えば、ファイルやネットワークからデータを次々に読む場合、 そのファイル相当に対して、 read() 的な API を何度も呼ぶことになるでしょう。

ここでは、標準入力経由で大きなデータをガンガン読み込む例を考えてみます。 例えば Go で書くとこんな感じになるでしょう。(エラー・終了処理は無視する)

package main

import (
    "fmt"
    "os"
)

func main() {
    file, _ := os.Open("./bigfile")

    for {
        data := make([]byte, 65536)
        n, _ := file.Read(data)
        fmt.Printf("%s", data[:n])
    }
}

Go ではファイルを読む方法はいくつかありますが、ここでは低レベル API を使うため、バッファを毎回確保し for を回してそこに入れています。 回さないと一回読み出してプログラムが終了してしまうので、 for が必要です。

しかし、これを Node のイディオムで書くと、以下のようになります。

var fs = require('fs');
var bigfile = fs.createReadStream('./bigfile');

bigfile.on('data', function(chunk) {
  console.log(chunk);
});

for や while、再起、 goto などは使っていません。 使っているのは、データが読み込まれたタイミングで発生するイベントです。

for を使っていないのだから、これを用いると出題である「for を使わずに 1~10 まで表示」という問題は、ちょっと変えれば解決します。

var fs = require('fs');
var bigfile = fs.createReadStream('./bigfile');

var c = 1;
bigfile.on('data', function(chunk) {
  console.log(c++);
  if (c > 10) {
    return bigfile.pause();
  }
});
// 1 2 3 4 5 6 7 8 9 10

さて、ではこれは本当に 「for を使ってない」と言えるのでしょうか? プログラム上出てきていないので、「このプログラムファイル内」では間違いなく使っていません。 しかし、実行時に「ランタイム」では繰り返し相当の処理が行われています。

それが、それこそが、「イベントループ」だ!!

イベント駆動とは何か

Node ではイベント駆動のモデルを採用しています。 先の例では、ランタイム内で「データが標準出力から一定量読み込まれた」という状態の変化が発生した時に「 data イベント」というイベントを発生させることによって、それを EventEmitter である、bigfile に通知しています。

ユーザランドでは、そのイベントが発生したときに実施したい操作を、コールバックとして登録しておくことで、イベント発生時の処理を実行しています。

イベントループとは何か

ではランタイムでは、データを読む部分はどうなっているのでしょうか?

実装で言うと、ランタイム内では最初の Go の例と同じように、ループを回して何度もデータを読み出しています。 Node の場合、実際は while 文であり、読み出したデータが一定量溜まったら、ユーザ側にイベントとして通知しています。

ブラウザの中でも考え方は同じで、例えば DOM に対してクリックが発生したかどうかは、内部の while ループの中でずっと監視しています。

この while ループのことをイベントループと呼んでいるといって、差支えないでしょう。

外部リソースに頼らない書き方

さて、さっきの例はファイルを読んでおり、外部からの情報に頼るとかちょっとチート感あるでしょう。

そこで、外部リソースを無くして、純粋にイベントループのみを利用するとこんな感じにも書けます。

process.on('count', function(c) {
  if (c > 10) {
    return;
  }
  console.log(c);
  process.emit('count', ++c); // B
});

process.emit('count', 0); // A
// 0 1 2 3 4 5 6 7 8 9 10

ちゃんとやるなら自分で EventEmitter を継承したものを実装すべきですが、手を抜いてグローバルな processEventEmitter であることを利用しています。 イベントループが回る最初の回で、 count というイベントにコールバックを設定し、その後すぐに登録したイベントを emit() (A) で発火しています。

イベントループの最後に emit() (A) で発火された count のコールバックが実行されます。その中でまた emit() (B) して、次のループの最後にまたコールバックが呼ばれ、また emit() (B) が実行されます。

よって、このソースコードに関しては for なしで命題を実装できているわけです。(同じことは、ブラウザでやるなら addEventListener()dispatchEvent() などで同じことができます。 ちなみに拙作ですが Node.js の events の移植 を使うこともできます。)

が、この実装では不十分です。 というか、こんな実装したらイスが飛んできてしまいます。

emit はブロックする

一行足せばわかりますが、 emit() は対応するコールバックが終わるまでブロックします。 つまり、そのコールバックの中でさらに emit() を呼ぶこの実装では、 emit() が終了待ちの列を作ります。

process.on('count', function(c) {
  if (c > 10) {
    return;
  }
  console.log(c);
  process.emit('count', ++c);
  console.log(c);
});

process.emit('count', 0);
// 0 1 2 3 4 5 6 7 8 9 10 11 10 9 8 7 6 5 4 3 2 1

要するにこれでは、実質的にただの「再帰」です。

そこで、毎回ループごとに emit() を実行してコールバックが終了するように、 process.nextTick() を使います。

ざっくり言えば、 process.nextTick() とは、このコールバックの実行を、「イベントループの次の周の最初」に実行するように登録する API です。

process.on('count', function(c) {
  if (c > 10) {
    return;
  }
  console.log(c);
  process.nextTick(function() {
    process.emit('count', ++c);
  });
});

process.emit('count', 0);
// 0 1 2 3 4 5 6 7 8 9 10

コールバックが emit() を予約して抜けるようになったイメージです。 コールバックが終わっても、イベントループの次の周でまた emit() が実行され、それを繰り返します。

ともかく、 emit() によるループのブロックは防げていることがわかります。

が、しかしこのコードには注意すべき点が二つあります。 一つ目は、同じ JS でもブラウザでは使えないこと。 二つ目は、 process.nextTick() が実行されるタイミングです。

process.nextTick() が無い場合

ブラウザは、イベントループがあるにもかかわらず process.nextTick() 相当の API が無いため、イベントループの次の周に処理を登録するには、 setTimeout() を使っていました。

setTimeout() はそもそも、イベントループに対して処理を登録し、ループが回るたびに「指定された時間が経過していたら実行」するための API です。

process.on('count', function(c) {
  if (c > 10) {
    return;
  }
  console.log(c);
  setTimeout(function() {
    process.emit('count', ++c);
  }, 0);
});

process.emit('count', 0);
// 0 1 2 3 4 5 6 7 8 9 10

したがって、 setTimeout(fn, 0) のように、 0 秒後にタイムアウトするようにすると、イベントループに登録し、次の周では指定時間が経過していると評価されて、コールバックが実行されます。

これで同様のことが実現でき、ブラウザにはかつてこの方法しかありませんでした。

setImmediate()

しかし、イベントループの次の周に登録することと、 0 秒後に登録するのでは、本質的な意味が違います。また、内部的にも実行されるタイミングが微妙に違います。そこでこの setTimeout(fn, 0) でやりたかったことを標準の API にしたのが setImmediate() です。

この API は現時点では、特定のブラウザ と Node.js の v0.9 以降で使用することができます。

process.on('count', function(c) {
  if (c > 10) {
    return;
  }
  console.log(c);
  setImmediate(function() {
    process.emit('count', ++c);
  });
});

process.emit('count', 0);
// 0 1 2 3 4 5 6 7 8 9 10

これで、少なくともこの目的であれば setTimeout(fn, 0) よりは、対応しているのであれば setImmediate() を使うことが推奨されます。 では setImmediate()process.nextTick() は Node 上では何が違うのでしょうか?

イベントループ上での実行タイミング

さて、同じようなことを行う API を二つ紹介しましたが、 Node の JS ランタイムの実装である V8 では、どちらもイベントループ上で実行されるタイミングが微妙に違います。

この辺については、 Object.observe()とNode.jsのイベントループの関係 - ぼちぼち日記 で詳細に解説されています。 エントリ中の図を引用します。

http://f.st-hatena.com/images/fotolife/j/jovi0608/20140320/20140320143241.png

この図からわかるこの二つの違いは、大きくは I/O イベントの前後どちらで実行されるか、という点です。

  • process.nextTick(): I/O コールバックの前
  • setImmediate() : I/O コールバックの後

これは、最初の大きなファイルからの読み込みと組み合わせて書いてみるとよくわかります。

まずは process.nextTick() を使った数え上げの後に、ファイルの読み込み(I/O)を書いた例です。 実行すると、数え上げが終わってからやっと I/O が始まっているのがわかります。

これは process.nextTick() が I/O よりも前に再帰展開されるからであり、その再帰が終わるまでイベントループがブロックされるていることを意味します。

var fs = require('fs');
var random = fs.createReadStream('./bigfile');

// I/O read
var c = 1;
random.on('data', function(chunk) {
  console.log('chunk');
  c++;
  if (c > 10) {
    return random.pause();
  }
});

process.on('count', function(d) {
  if (d > 10) {
    return;
  }
  console.log(d);
  process.nextTick(function() {
    process.emit('count', ++d);
  });
});

process.nextTick(function() {
  process.emit('count', 0);
});

// 0 1 2 3 4 5 6 7 8 9 10 chunk chunk chunk ...

同じコードを setImmediate() に一カ所だけ書き換えてみます。 すると、 I/O 処理の後に数え上げ処理が実行されるため、両者がおおむね順番に実行されていることがわかります。 (ただし、 I/O の制御はカーネルが行っているため、イベントループ中毎回必ずデータが読まれるとは限らないため、交互とはならないことがあります。)

var fs = require('fs');
var random = fs.createReadStream('./bigfile');

// I/O read
var c = 1;
random.on('data', function(chunk) {
  console.log('chunk');
  c++;
  if (c > 10) {
    return random.pause();
  }
});

process.on('count', function(d) {
  if (d > 10) {
    return;
  }
  console.log(d);
  setImmediate(function() {
    process.emit('count', ++d);
  });
});

setImmediate(function() {
  process.emit('count', 0);
});
// 0 chunk 1 chunk 2 chunk 3 chunk 4 5 chunk...

そして、これは実は再帰じゃありません。イメージとしては、while ループの処理を動的にオン/オフしているイメージでしょうか?

イベントループハラスメント

先ほどの process.nextTick()再帰のように同期処理によってイベントループを止めるというのは、その後の I/O 処理までループが到達しなくなってしまうことを意味します。 これは、例えば再帰フィボナッチ数列を出すような処理を Node で行うと、そこでイベントループが止まり、非同期 I/O を止めてしまうという意味であり、こうしたイベントループを止めてしまうような処理を「イベントループハラスメント」と言った人もいました。

http://img.f.hatena.ne.jp/images/fotolife/J/Jxck/20111226/20111226023227.png

ネットワーク系、特に Web 系のプログムでは、 I/O 周りがボトルネックになることが多いことから、この点を解決するために非同期 I/O を採用し、イベント駆動にするためにイベントループを回している Node にとって、そのループを止めるのは全てを台無しにする行為なので、そこを理解した上でイベントループを止めないようにするのが重要です。

そして、その一つのイベントループ、正確にはシングルスレッドで回っている while ループの上に処理を登録して、ループごとに完了した I/O を処理することができるのが Node のメリットであり、それが重たい I/O の待ち時間を無駄なく使うことができるアーキテクチャだったため、たくさんのスレッドを立ててロックを取り合い処理するモデルよりもシンプルで省メモリだったのが Node が他のランタイムと違ったところです。

イベントが発火した「後」に処理を登録するためには、確かにコールバックが必要です。コールバックが無いモデルを好む気持ちはわかりますが、イベント駆動でない限り、多くのランタイムは自分でこのイベントループ相当の while を回し、そこにフラグと処理を書くことになるとは思います。それたぶん Node がやってくれてることかもしれません。

(イベントループを止めずに、一周に一回フィボナッチを計算するという処理を setImmediate() で書くこともできます、やってみてください。)

for を使うなイベントループを使え

JavaScript では、あなたが for を回さなくても、イベントループはいつでもそこで回っています。 それを止めないように、それに乗っかるように、あなたが書こうとした for が本当に必要か考えてみてはいかがでしょう?

ジェネレータの解説と非同期への適用

update

2014-01-16

ご指摘頂いたので修正しました。ありがとうございます!

https://gist.github.com/Jxck/8380852 は修正済みです。動画の取り直しは勘弁して下さい(汗

-      fn.apply(fn, args);
+      fn.apply(this, args);

intro

あけましておめでとうございます。 今年からはてなブログへ移行しました。

去年末くらいから流行っている Express の後継 Koa では JS の新機能ジェネレータが使われています。 このジェネレータは現在最新の node@11.10 などであれば --harmony-generator をつけることで、有効にすることができます。

今回はこのジェネレータの使いかたと、それを用いてどうやって非同期処理が書き換えられるかを、実演しながら解説したいと思います。

ちょっと長くなってしまったのですが、だいたい以下のような感じです。

  • ~7:00:ジェネレータと yield と next() の使い方と、 next() と yield で値を受け渡す時の注意。
  • 7:00~:ジェネレータで非同期をどうやって縦に書けるか、またエラーはどう処理するか、の解説。

なので、ジェネレータ自体がわかっている人は、 7:00 くらいから見ても大丈夫です。

これを見終わると、今流行?のあのライブラリの仕組みがなんとなくわかるかと。

Movie

how to use generator in node from Jxck on Vimeo.

完成品

https://gist.github.com/Jxck/8380852

まとめ

Generator の特徴は、処理を任意の時点で止めてそこから再開できることです。 これを上手く用いると、非同期処理の実行で一旦止め、コールバックが終了したら再開するということができます。

この時、 yield と next() で値をやりとりできるので、コールバック内でしか触れなかったデータ (err, data など) を、コールバックの外の世界に出すことができるようになりました。すると、これまでコールバックの世界でやっていたことを、わざわざコールバックの中でやらなくて良くなるため、コールバックの中でやるべきことは「コールバックの中で next(data) して、コールバックの外にデータを出す」という定形処理だけで良くなります。

Node の API基本的にコールバックを引数の最後に取るので、これを上手く利用して、ユーザランドではコールバック以外の引数だけを設定し、定形のコールバックのライブラリ側で実行する。この時、コールバックに渡るデータは、 yield の評価結果としてジェネレータ内に戻し、エラーは throw() を用いて、ライブラリからジェネレータ内に戻します。(Generator 内では、 try-cache で取れます)

この、「Generator の世界とコールバックの世界の、データの橋渡し」と「定形コールバックの実行」を行っているのが co です。 そして、それによってユーザランドでは意識しないでよくなるコールバックを、 Node の API から消し去ってくれるのが thunkify です。

補足

ただし、この中で出てくる co, thunkify もどきは、本家のと比べればおもちゃ以下なので、 本家が中で何をやっているのかを知る手がかりとして見て頂ければと思います。 (本家を読むきっかけくらいにはなるかと。)

本当は co は thunkify 以外にも promise を上手く扱えるようになっているので、気が向いたら promise バージョンを撮ってみようかと思ってます。

harmony オプションが解禁されると、選択肢は非常に増えます。 去年は Koa と Angular な年感あったかもしれませんが、自分にとって今年はもう一段下の Object Observe とジェネレータの年になりそうだと思っています。

JSON - を node の Stream で整形する

intro

ちょっと反応が遅れてしまいましたが。


404 Blog Not Found:JSON - をnodeで整形する


こちらの記事は Stream 厨として見逃す訳にはいきませんでした。

motivation

まあ、 JSON ですしね。

  • そのJavaScriptに今や標準搭載のJSON.stringify()は実はpretty printできるし

確かに stringify でできますね。(昔こちらにも書きました。)

  • どうせなら標準入力だけではなくURIやファイル名で直アクセスしたいし

同意です。


しかし、実装を見てみると、、
(以下、主要な部分の抜粋)

// stdout
stdin.on("data", voorhees);
// http
http.get(req, function (res) {
  res.on('data', function (data) {
    chunks.push(data)
  }).on('error', function (e) {
    console.log(e.message);
  }).on('end', function () {
    voorhees(chunks.join(''))
  });
});
// fs
fs.readFile(argv[2], function (err, data) {
  if (err) throw err;
  voorhees(data);
});


実は、こうした「データソースから受け取ったデータを加工して、次に渡す」といったものこそ、 Stream に向いています。
正確には、今回の用途は「データをまとめて扱っている」ので、 Stream にする必要はかならずしもないです。


しかし、 Stream にしておくと以下のようなメリットがあるかと思います。

  • すでに Stream として抽象化されているものとの接続製があがる。(substackdominic のライブラリ集が使える!)
  • うまくやれば、 JSON のストリーム(次々と途切れなく流れてくる JSON の配列とか) を、途中途中で Stringify しながら画面表示したりできる。
  • voorhees は CLI を提供するフロントの層と JSON を整形するロジックの層が1つになっています(それ自体をどうこう言う気はないです) が、こうしたレイヤの分離も Stream を使うとうまくいったりします。


そしてなにより、先の 3 つのリソース(stdin/out, file, http) は全て、標準ですでに Stream として抽象化されています。


乗るしか無い、このエコシステムに!

Transform Stream

今回の用途だと、 Transform Stream にすることになります。特徴だけ説明します。


今回は、チャンクごとに処理せず、まとめて最後に処理するので、 _transform() はデータを溜めるだけで、 _flush() で全部 stringify() 処理します。

JSPP.prototype._transform = function(chunk, output, cb) {
  chunk = chunk.toString();
  if (chunk) {
    this.data += chunk;
  }
  return cb(null);
};

JSPP.prototype._flush = function(output, cb) {
  if (this.data) {
    try {
      output(this._pp(this.data));
    } catch(e) {
      return cb(e);
    }
  }
  cb(null);
};


使う場合は、 pipe するだけです。

// 標準入出力の場合
process.stdin.pipe(jspp).pipe(process.stdout);


// http の場合
http.get(url, function (res) {
  res.pipe(jspp).pipe(process.stdout);
});


// ファイルの場合
fs.createReadStream(file).pipe(jspp).pipe(process.stdout);


こうして、標準モジュールや、他の Stream を実装したライブラリと、好き放題繋ぐ事ができます。

jspp-stream

ということで、以前作った ltsv-stream をちょちょっといじって、 jspp(json pritty print) stream ということで実装してみました。


https://github.com/Jxck/jspp-stream


Stream 実装(jspp-stream.js)だけでなく、これを使った voorhees 互換の jspp コマンドも同梱してあるので、 npm -g で入れれば使えます。
(こうして、ロジックの層とフロントの層が簡単に分けられるのも、 Stream を使うメリットの1つです)

$ npm install -g jspp-stream
$ cat test.json | jspp
$ jspp /path/to/file.json
$ jspp http://some.json.com


テストもエラー処理も適当ですが、まあこんな感じでできますよということで。

LTSV の Stream Parser を Stream2 で書いてみた

Update

2013/02/12
JSON => JSON Object に(JSON string でないものは)修正

LTSV

LTSV が流行っていたんですが、完全に乗り遅れて Node も Go も実装は出てしまいました。
Node の方は sasaplus1 さんのものが こちら にあるんですが、パーサ関数のみで Stream ではなかったので、 Stream 実装を書いてみました。

ltsv-stream

Jxck/ltsv-stream · GitHub


npm でインストールできます。

npm install ltsv-stream

Stream2

Node での Stream の重要性は、このブログでも何度か書いてきたと思いますが、この Stream は Stream2 という新しい実装に変わりつつある (Stability: 2 - Unstable, v0.9 以降) ので、今回は Stream2 の勉強がてらそちらで書きました。

Stream2 のドキュメントは v0.9 以降の Node.js ドキュメント を見て下さい。


Stream2 の実装方法はこれまでの Stream と違い、用意されたクラスを継承して、親クラスの Abstract Method を小クラスで Override するという Template Method パターンに則って実装することになります。

Transform Stream

今回開発する Stream は LTSV 形式の文字列を読み込んで、各行を JSON Object にパースするようにします。
つまり、左から右にデータを変換しながら流すので Transform Stream (かつてこのブログでは Filter Stream といっていたもの) になります。


今回使ったライブラリは、 Transform Stream として以下のように使えます。
LTSV 形式のログが詰まったファイルが、流れるようにパースされて表示されている様が手に取るようにわかるでしょう。

var ltsv2json = require('ltsv-stream').ltsv2json
  , fs = require('fs');

var ltsv = new ltsv2json({stringify: true});
fs.createReadStream('ltsv-access.log').pipe(ltsv).pipe(process.stdout);


この例では表示のために stringify するオプションを使ってますが、
false の場合(デフォルトなので書かなくて良い)は JSON Object が emit されます。
(このオプションは ltsv-stream 独自のオプションで stream2 標準ではありません。)


本来このライブラリは、このあと何か LTSV 変換した JSON Object を使う Readable Stream を実装して、それを pipe() して使うような用途を想定しています。
stringify: true はネットワークに直接流すときや、デバッグ用に使って下さい。


Transform Stream の実装には二つのメソッドを Override します。
今回はこの二つが ltsv-stream でどう使われているかを解説します。

  • transform._transform(chunk, outputFn, callback)
  • transform._flush(outputFn, callback)
_transform

このメソッドが Transform の核となるメソッドです。
今回の場合は LTSV を一行ごとに JSON Object にする処理を書きます。


入力は文字列を想定していますが、一行づつ入力されることは想定していません。
例えば先の例のように fs.ReadableStream を pipe すると、入力はどこで途切れるかわからないので、今回は各行に分けてから LTSV のパース処理をする必要があります。


今回の場合は、簡単にするとこんな感じ。

LtsvStream.prototype._transform = function(chunk, output, cb) {
  if (chunk) {
    this.line += chunk;
    while (this.line.match(/\r?\n/)) {
      var record = RegExp.leftContext;
      this.line = RegExp.rightContext;
      record = this.parse(record); // ここで LTSV -> JSON Object
      output(record);
    }
  }
  return cb(null); // chunk の処理を終了
}


引数の chunk は、処理対象となる chunk です。今回の場合は LTSV の中途半端な文字列です。
もしかしたら LTSV 複数行分かもしれないし、一行に満たないかもしれません。
それを意識した上でパースして、生成された JSON Object を output() に渡します。
渡した分だけ次の Readable Stream に渡されます。


一つの chunk が処理し終わったら、 cb() を呼びます。
cb() にはその中で発生したエラーを渡すこともできます。
cb() を呼んだら次の chunk の処理に入り、以下繰り返しです。


_flush

_flush() は上位クラスで end イベントの前に呼ばれます。
Stream の終端で、最後にやらなければいけない作業がある場合、このメソッドを override することで実現できます。


今回の場合は、データを行ごとに処理するために chunk を一旦内部に保存しているので、 _transform で output() されなかったデータが残っている可能性があります。


今回は、この残ってるかもしれないデータを _flush() で処理します。
残ってるのは一行かそれに満たない LTSV レコードのはずなので、とりあえずまるっと parse() してみるだけです。
処理が終わったら(もしくは無かったら) cb() を呼び終わります。

ltsv2json.prototype._flush = function(output, cb) {
  if (this.line) {
    var record = this.line;
    try {
      record = this.parse(record);
    } catch (e) {
      return cb(e);
    }
    output(record);
  }
  cb(null);
};

Stream のテスト

今回の Transform Stream のテストはとりあえず以下のように書いてみました。
SPY (今思うと SPY とはちょっと違った。。まあいいや。) となる Writable Stream を作成し、それを pipe して中で渡されてくる値を確認し、 end イベントで終わらせる方針です。

SPY stream

チェックだけする Writable Stream です。

var Writable = require('stream').Writable;
var util = require('util');

function SPY(options) {
  Writable.call(this, options);
}
util.inherits(SPY, Writable);

// _write を上書きして、 chunk を調べる。
SPY.prototype._write = function(chunk, cb) {
  assert.deepEqual(chunk, expected);
  cb(null);
};
test

これを用いたテストは以下のようになります。
mocha の場合は非同期テストのための done() (このブログで紹介した next() と同等) を受け取れるので、 spy の finish イベントでそれを呼ぶことで、全ての chunk をテストできます。

var ltsv = new ltsv2json({stringify: false});
var spy = new SPY();
fs.createReadStream('test/test.log')
  .pipe(ltsv)
  .pipe(spy)
  .on('finish', function() {
    done(); // ここで終わる
  });

まとめ

ということで、 LTSV の波には見事乗り遅れましたが、ずっとペンディングしていた stream2 デビューを果たすことができました。


まだまだ Stream2 はドキュメントも少なく、実装も議論中なところがあるので手を出す敷居は高いかもしれません。
自分もまだまだ手探りなオプションや挙動が色々あります。
しかし、 Stream を制すものは Node を制すということで、自分ももう少し触って勘所を掴んでいきたいと思います。

東京 Node 学園祭 2012

intro

去年に引き続き 東京Node学園祭2012を開催しました。
今年も、スタッフとして参加させて頂きました。


今年は去年より規模を大きくし、海外のゲストも 4 人呼ぶことができました。
個人的には孤高のハッカー substack と行動したのが思い出深いですw


一緒に運営をしたスタッフの皆さん、出て頂いたスピーカーのみなさん、
法政大学さん、スポンサーの皆さん、
ワールドツアーで忙しい中日本に来てくれた izs, mikeal, charlie, substack
みなさんありがとうございました!

omake

substack に「学園祭のロゴ書いてよ」っていったら、おもむろに書き始めた図。
(途中で料理が来てしまったので中断。完成版を送ってくれると良いなぁ。)

substack drawing from Jxck on Vimeo.

サーバサイドJavaScript Node.js入門 を執筆させて頂きました。

intro

本当にお待たせいたしました。


出す出すといってなかなか出せなかった Node.js 本が、ついに出版されました。
共著の一人として、自分も書かせていただいています。


http://pbs.twimg.com/media/A6Q2xmrCAAEsFxi.jpg


サーバサイドJavaScript Node.js入門

サーバサイドJavaScript Node.js入門


これを書いてる時点では、 JavaScript 部門では 1 位プログラミング部門では 4 位 となっているようです。
それなりに、興味を持っていただけているようで良かったです。


自分の記録としても、少しだけ書かせて頂きます。

2 年間の執筆

書き始めたのは、かれこれ 2 年前になります。
その頃はまだ node.js@0.2 とかでした。(今は 0.9, もうすぐ 1.0)


本当は年が明けた春(2011/4 とか)に出そうって話で書き始めたんですが、
実際はそうはいかなかったです。


一番の理由は、僕らが書くと同時に Node とその周辺自体がどんどん新しくなることです。
書いてるそばから内容がどんどん古くなる。これは結構書いてて辛かったです。


なるべく使える情報を届けたいけど、一方で書きなおしを続けててもエンドレスになってしまうということで、
何度も話し合いながら、ここだけは対応したい、というところを直しながら走り続けてきました。


自分が担当した章では、書き始めから(ブランチ数で)約 20 回くらい書きなおした章もあります。


もしただ単純に執筆をサボってて遅れてただけなら、きっとこの本の話自体が消えていたでしょう。
むしろ、この本の企画をゴールまで守って頂いた嘉平さんには、本当に感謝します。


Node@1.0 のリリースとか、学祭の直前云々のタイミングに合わせず、今このタイミングで出版したのも、
今ちょうど、少しでも早く出すための色々な条件が重なったからです。
もしこのタイミングを逃してたら、今頃まだ書き直しを繰り返して、どの段階で出すべきか議論してたかなと思います。


とはいえ、読者の方からすれば中々出なかったという事実だけなので、
そんなのは関係ないですね。その点に関しては本当に申し訳なかったと思っています。

書き直し

ちなみに自分が覚えている、「これは書き直し、書き足しが必要かな。。」ということになった、
大きい変更はざっとこんなのがあったなと思い出します。

  • npm が標準になる
  • windows 対応が始まる
  • libuv が出てくる
  • Cluster が標準になる TJ の Cluster がオワコンになる。
  • Isolate が出そうで無くなる
  • Express が 2.0 になり、色々変わる。
  • Express に routes ディレクトリができる。
  • Expresso がオワコンになり、 mocha + chai になる。
  • Socket.IO に色々機能が増える
  • Domain が入る
  • etc, etc, etc....


思い出すだけでも目から汗が。。

API より考え方

最初の頃は、色々な場面で API について書きすぎたのが間違いだったなと途中で気づきました。
あと、やっぱみんなそれぞれ詳しいモジュールとかあるし、ソースとかも読んでから書いてるので、
ドキュメントに載ってない API についてとか、マイナーな設定方法とか書いてしまっていました。


でも、 API は変わるんですよね。しかも最初の段階は割りと早く。


それがバージョンに追従するための書き直しに大きく響いていたし、
それは同時に内容の寿命を縮めていることに気づきました。


だから、途中からもっと「考え方」にフォーカスして書きなおすようにしてます。


もちろん、モジュールを使ってる章は API について(ときには本家のドキュメントより)詳細な解説をしているんですが、
なるべく「そのモジュールのバージョンが上がっても、変わらないだろう "考え方"」を書くようにしました。


どっちにせよ、この本が出たあとも、ものすごい速さで Node は進んでいくと思いますが、
その流れに乗るために必要なものを、この本から得て頂ければいいなと思っています。


ちなみに、manning のように、電子書籍でアップデートしながら走るという方法については、本当に多くの方に言われます。
実際自分たちも最初の段階から話はしました。ここには書けないですが、それはそんな単純でもないかなと未だに思います。

写経

この本の特に「実践編」は写経されることを意識して Diff で書いています。
この本を読むほとんどの方には、その方がわかりやすいでしょう。
ちょっと慣れが必要かもしれませんが、慣れれば開発の過程を追体験しながら、
写経することができます。


使用する Node と基本全てのモジュールはバージョンが明示されているので、
モジュールがどんどん新しくなっても、
同じバージョンを指定して npm でインストールすれば、同じように動くはずです。


最終形態とその他のリソースは、出版社のサイトから落とせるようになるので、
そちらも合わせて参考にしてみて下さい。

おわりに

今のところ、世界でも珍しいくらい Node についてガッツリ書いてあります。
海外でこの話をすると、みんなに「英訳は出ないのか?」と言われるとか。


必要なことは大体書いてあるはずなので、
これを期に、 Node に少しでも興味を持って頂けるといいなと思います。


書ききれないこと色々あるけど、この辺にしておきます。


最後に、
一緒に執筆させていただいた著者の皆さん
レビューしていただいた皆さん
担当いただいた嘉平さん


本当にありがとうございました。


see also: http://meso.hatenablog.com/entry/2012/10/26/111330

非同期と next()

intro

この記事は、 東京Node学園祭2012 アドベントカレンダー : ATND の 3 日目の記事です。
(日付変更線は、はてな時間を採用しております。。 ごめんなさい m(__)m)

非同期と next()

next() とは、 Express や Connect 、 Mocha 、 そして一部のフロー制御ライブラリで使われている、
関数のことをさしています。

今回は、これがどういう機能で、どういうふうに実装されているのかをスクリーンキャストで紹介します。

まとめ

今回書いたコードはこちらです。

https://gist.github.com/3906371

実装方法は色々あるかと思いますが、だいたいこんな感じで実装できて、使うことができる、
わかってしまえばそんなに難しくない。といったところが少しでも伝わればと思います。


数多あるフロー制御モジュールがどうなっているのかを読んでみたり、
オリジナルのフロー制御モジュールなんかを作るきっかけになればと思います。