TameJS による継続と CoffeeScript 上での利用 - 非同期処理でWebコンテンツをダウンロードする方法4
TameJS は JavaScript で継続(Continuation)を実現するためのツールです。
TameJS によって Scala の限定継続(id:fits:20111016 参照)のような事ができるので、コールバックを多用する Node.js の処理を比較的シンプルに実装できるようになります。
そこで今回は、id:fits:20111030 で実装した Web コンテンツダウンロード処理(Node.js 版)を TameJS と TameJS + CoffeeScript で実装してみました。
- Node.js v0.6.14
- CoffeeScript 1.3.1
- TameJS
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20120415/
事前準備
まずは npm を使って tamejs モジュールをインストールしておきます。(Node.js と CoffeeScript はインストールされているものとします)
> npm install tamejs -g
なお、-g オプションを付けなかった場合、カレントディレクトリの node_modules ディレクトリ内にインストールされますので、この場合は ./node_modules/.bin を環境変数 PATH に追加して使用します。
TameJS の場合
TameJS では、await ブロック内に非同期処理を実装しコールバックに defer を指定する事で、コールバックが実行されると await ブロックの終わりから処理が継続され、defer に指定した変数*1が使えるようになります。
Scala 限定継続の shift が TameJS では await に、継続の呼び出しが defer に該当するようなイメージです。
download_web_tamejs.tjs
var http = require('http'); var url = require('url'); var fs = require('fs'); var path = require('path'); var dir = process.argv[2]; var printError = function(urlString, error) { console.log('failed: ' + urlString + ', ' + error.message); } process.stdin.resume(); await { //コールバック(defer)が実行されると (1) へ process.stdin.on('data', defer(urls)); } //(1) urls.toString().trim().split('\n').forEach(function(u) { var trgUrl = url.parse(u); await { //URL接続成功後にコールバック(defer)が実行されると (2) へ http.get(trgUrl, defer(var res)).on('error', function(err) { printError(u, err); }); } //(2) res.setEncoding('binary'); var buf = ''; await { //データ取得 res.on('data', function(chunk) { buf += chunk; }); //データ取得が完了しコールバック(defer)が実行されると (3) へ res.on('end', defer()); res.on('close', function(err) { if (err) { printError(u, err); } }); } //(3) var filePath = path.join(dir, path.basename(trgUrl.pathname)); await { //ファイルへの出力が完了しコールバック(defer)が実行されると (4) へ fs.writeFile(filePath, buf, 'binary', defer(var err)); } //(4) if (err) { printError(u, err); } else { console.log('downloaded: ' + u + ' => ' + filePath); } });
上記の .tjs ファイルを tamejs コマンドで .js に変換した後、node コマンドで実行します。
実行
> tamejs download_web_tamejs.tjs > node download_web_tamejs.js destdir < urls.txt
TameJS + CoffeeScript の場合
CoffeeScript で TameJS を使うには Embedded JavaScript を使います。
具体的には、await { と } の箇所を ` で囲み、以下のように await ブロック内の処理は字下げしないようにします。
正しい記述
`await {` process.stdin.on 'data', defer(urls) `}`
誤った記述 (以下のように字下げすると coffee コマンド実行時にエラーが発生します)
`await {` process.stdin.on 'data', defer(urls) `}`
CoffeeScript 上で TameJS を使って実装したダウンロード処理は以下のようになります。
download_web_tamejs2.coffee
http = require 'http' url = require 'url' fs = require 'fs' path = require 'path' dir = process.argv[2] printError = (urlString, error) -> console.log "failed: #{urlString}, #{error.message}" process.stdin.resume() `await {` process.stdin.on 'data', defer(urls) `}` urls.toString().trim().split('\n').forEach (u) -> trgUrl = url.parse u `await {` # URL 接続 http.get(trgUrl, defer(res)).on 'error', (err) -> printError u, err `}` res.setEncoding 'binary' buf = '' `await {` # データ取得 res.on 'data', (chunk) -> buf += chunk # データ取得完了 res.on 'end', defer() # 接続後のエラー処理 res.on 'close', (err) -> printError trgUrl.href, err if err `}` filePath = path.join dir, path.basename(trgUrl.pathname) `await {` # ファイル出力 fs.writeFile filePath, buf, 'binary', defer(err) `}` if err printError trgUrl.href, err else console.log "downloaded: #{trgUrl.href} => #{filePath}"
実行するには、まず coffee コマンドで .js に変換し(実際には .tjs の内容)、tamejs コマンドで .js に変換して node コマンドで実行します。
なお、coffee コマンドで -b オプションを指定しておかないと tamejs コマンド実行時にエラーが発生する点にご注意下さい。
実行
> coffee -b -c download_web_tamejs2.coffee > tamejs -o download_web_tamejs2.js download_web_tamejs2.js > node download_web_tamejs2.js destdir < urls.txt
ちなみに、tamejs は入出力に同じファイル名を指定できるので、上記では coffee コマンドで出力されたファイルを tamejs コマンドの処理結果で上書きしています。
CoffeeScript の場合
最後に、TameJS を使わずに CoffeeScript だけで実装したものは以下の通りです。
download_web.coffee
http = require 'http' url = require 'url' fs = require 'fs' path = require 'path' dir = process.argv[2] printError = (urlString, error) -> console.log "failed: #{urlString}, #{error.message}" process.stdin.resume() process.stdin.on 'data', (urls) -> urls.toString().trim().split('\n').forEach (u) -> trgUrl = url.parse u # URL 接続 req = http.get trgUrl, (res) -> res.setEncoding 'binary' buf = '' # データ取得 res.on 'data', (chunk) -> buf += chunk # データ取得完了 res.on 'end', -> filePath = path.join dir, path.basename(trgUrl.pathname) # ファイル出力 fs.writeFile filePath, buf, 'binary', (err) -> if err printError trgUrl.href, err else console.log "downloaded: #{trgUrl.href} => #{filePath}" # 接続後のエラー処理 res.on 'close', (err) -> printError trgUrl.href, err if err req.on 'error', (err) -> printError trgUrl.href, err
実行
> coffee download_web.coffee destdir < urls.txt
*1:コールバックの引数