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:コールバックの引数