開発^3

Web開発、宇宙開発、ゲーム開発の3種類についてつらつらと

caolan/asyncを使ったNode.jsのフロー制御

JavaScript Advent Calendar 2011 (Node.js/WebSocketsコース)の16日目です。

@koichikさんの13日目の記事にも書かれているように、Node.jsにて非同期処理を管理する方法には以下の2種類があります

  • 非同期処理の呼び出し時にコールバック関数を与える。処理が終わったらエラーの有無と、結果が通知される。
  • EventEmitterに対してon/onceでEventListenerをセットして非同期処理を実行する。非同期処理の実行によって発生したイベントやエラーはリスナーに通知される。

後者の説明はkoichikさんの記事にお任せして、この記事は前者を扱うライブラリcaolan/asyncについてみていきたいと思います。

caolan/asyncって?

https://github.com/caolan/async で公開されているフロー制御ライブラリです。
自分はNode.jsで使っていますが、ブラウザ上で動くJavaScriptを書く場合にも使用できるようです。

https://github.com/joyent/node/wiki/modules#wiki-async-flowに記載されているNode.js用フロー制御モジュールは既に50を超えていますが、試してみた中では一番使いやすいモジュールでした。
# さすがに50すべては試せていないです。githubのwatcher数1000オーバー、folk数62ということで少なくともフロー制御モジュールの最大手ではあるはず。

asyncが実現してくれるフロー制御の一部はまさに「非同期プログラミングの改善」のエッセンスの内容なので、先に読んでおくと理解が深まるかと思います。

何をしてくれる?

asyncがやってくれる処理には様々なものがありますが、まずはasync#seriesを例にして説明していきたいと思います。
hogeディレクトリを掘って、piyoディレクトリを掘って、その下にデータを書きだすコードをコールバックを使って書くと以下のようになります。
(ディレクトリ堀は再帰的にやってくれるモジュールがありますが見なかった方向で)

var fs = require('fs');
fs.mkdir('./hoge', function(err) {
  if(err) {
    // エラー処理
    return;
  }
  
  fs.mkdir('./hoge/piyo', function(err) {
    if(err) {
      // エラー処理
      return;
    }   

    fs.writeFile('./hoge/piyo/neko', 'Hello Node.js', function(err) {
      if(err) {
        // エラー処理
        return;
      }   
      console.log('success');
    }); 
  }); 
});

この例のように、非同期処理の終了を待ってから次の処理をさせる場合、ネストが深くなる、エラー処理があちこちに分散するといった問題点があります。
では、このコードをasyncを使った形で書き直してみましょう。

var fs = require('fs');
var async = require('async');

var tasks = []; 
tasks.push(function(next) { fs.mkdir('./hoge', next); }); 
tasks.push(function(next) { fs.mkdir('./hoge/piyo', next); }); 
tasks.push(function(next) { fs.writeFile('./hoge/piyo/neko', 'Hello Node.js', next); }); 

async.series(tasks, function(err, results) {
  if(err) {
    // エラー処理
    return;
  }
  console.log('success');
  console.log(results);     //  [undefined, undefined, undefined]
});

はい、エラー処理が一カ所にまとまりました。
ネストが深くなることもなく、処理もtasksにまとまっていて良い感じです。

async#seriesは第一引数で与えられたtasksの処理を順番に実行してくれるメソッドです。
tasksに格納された各処理は、自分の処理が終わった時点でnext()を呼び出し、「エラーの有無」と「自分の処理結果」を通知します。
エラーが発生しなかった場合には自動的に次の処理が呼び出され、 エラーが発生した場合には、それより先の処理は実行されずにasync.seriesの第二引数に渡された関数が呼び出されます。
errには発生したエラー(エラーなしの場合はnull)が入り、resultsには各処理の処理結果がまとめて入ります。
今回使ったfs#mkdirやfs#writeFileはエラーの有無のみを通知する為、resultsに関しては次項の例を見てもらった方が分かりやすいかと思います。*1

async#series, async#parallel

asyncモジュールにはseries以外にも様々なメソッドが用意されています。
ここからは、それらの処理の流れを図で表しながら簡単に説明していきたいと思います。

まずはasync#seriesです。
先ほど説明した通り、一つの処理が終わってから次の処理が行われ、それぞれの処理結果がendに集約されています。


凡例:
・青い四角:処理
・赤い四角:データ
・青線:処理の流れ
・赤線:データの流れ
・finish:async#****を呼び出すときに与えられるcallback関数
(エラー発生時か、全部の処理が終わった後に呼ばれる関数)

# 図はCacooにて作成。Excelは図を描くツールではないのです。

つづいて、async#parallel。こちらは全部の処理が同時に実行され、すべてが終わった時点でendが呼ばれます。
この場合もseriesと同じく、それぞれの処理結果はendに集約されています。

async#seriesとasync#parallelの違いを見るために「delayミリ秒待ってからtextの内容を出力し、処理結果としてresultを返す関数」を作って試してみます。

function delayedPrint(delay, text, result) {
  return function(next) {
    setTimeout(function() {
      util.log(text);
      next(null, result);
    }, delay);
  }
}

var tasks = []; 
tasks.push(delayedPrint(3000, 'test1', 'result1'));
tasks.push(delayedPrint(2000, 'test2', 'result2'));
tasks.push(delayedPrint(1000, 'test3', 'result3'));
async#seriesの場合
util.log('start');
async.series(tasks, function(err, results) {
  util.log('true end');
  console.log(results);
});

util.log('end?  ... NO. while processing.');

実行結果:

16 Dec 01:39:13 - start
16 Dec 01:39:13 - end?  ... NO. while processing.
16 Dec 01:39:16 - test1
16 Dec 01:39:18 - test2
16 Dec 01:39:19 - test3
16 Dec 01:39:19 - true end
[ 'result1', 'result2', 'result3' ]
async#parallelの場合
util.log('start');
async.series(tasks, function(err, results) {
  util.log('true end');
  console.log(results);
});

util.log('end?  ... NO. while processing.');

実行結果:

16 Dec 01:40:15 - start
16 Dec 01:40:15 - end?  ... NO. while processing.
16 Dec 01:40:16 - test3
16 Dec 01:40:17 - test2
16 Dec 01:40:18 - test1
16 Dec 01:40:18 - true end
[ 'result1', 'result2', 'result3' ]

これらの結果から以下のことが分かります。

  • end?の時点ではまだ処理が終わっていない。全部の処理が終了したのはtrue endの時点。
  • startからendまでの時間がseriesでは直列実行の為、3 + 2 + 1 = 6秒
  • startからendまでの時間がparallelでは並列実行の為で、max(3, 2, 1) = 3秒
  • parallelの例を見るとわかるようにresultsに入ってくるのは処理の完了順ではなくtasksの順

async#waterfall

async#waterfallを図に表すと以下のようになります。

処理の流れ自体はseriesと同等ですが、seriesと違い、処理結果が次の処理に与えられます。
便利なメソッドであることは確かなのですが、このメソッドに関しては気になっていることがある為、後日改めてエントリを起こそうと思います。

async#forEach, async#forEachSeries

前項で紹介したseries, parallel, waterfallは複数の処理の流れを制御する形でしたが、配列の各要素に対して同じ処理を実行することも良くあります。
同期処理であればArray#forEachを使えばよいのですが、その中に非同期処理が含まれている場合、Array#forEachから処理が返った時点では配列の各要素に対する処理が終わっていません。
asyncではこういった場合に使えるメソッドとしてforEach, forEachSeriesというメソッドを用意しています。

名前からもわかるようにforEachは並列処理を行い、forEachSeriesは直列に処理を行います。



async#map, async#mapSeries

forEachやforEachSeriesは配列の各要素に対して処理を行いますが、処理結果を返す機能は持っていません。
配列の要素を変換したい場合などにはArray#mapと同じく、async#mapを用います。
これもforEachと同じくasync#mapとasync#mapSeriesの二つが用意されているため、変換ロジックの重さに応じて好きな方を選ぶことができます。



ほかの関数

今回は紹介しきれませんでしたが、caolan/asyncには他にも以下のようなメソッドを提供しています。

  • makeファイルのように各タスクに前提条件を指定すると、自動的に順番を組み立ててくれるauto
  • forEach/mapのようにArrayが提供していた処理をやってくれるfilter, filterSeries, reject, rejectSeries, reduce, reduceRight, concat, concatSeries, some, any, every, all
  • 条件を満たす間 or 満たすまで処理を続けるwhilst, until
  • 並列で実行する最大数を指定して大量のタスクを処理させられるqueue
  • 重い処理の処理結果を自動的にキャッシュして二回目以降の呼び出しを軽くするmemoize

これらのメソッドとEventEmitterをうまく使うことで処理の流れを整理し、処理の流れが分かりやすいコードを目指しましょう!

明日は@n_matsuiさんです。

*1:例選びに失敗した感がひしひしと

MacBookAirでWindowsを使う場合のキー設定

MacBookAirにBootCampでWindowsを入れて使っていますが109キーボードとは形状がことなる為、特殊キーの入力方法がややこしかったり、違和感があったりします。
自分が設定した内容をまとめてみました。

特殊キーの入力方法

まずは押し方次第で入力できる特殊キーの一覧。
Pause, Insert, PrintScreen, ScrollLockあたりは一風変わったキー入力ですね。

Windows環境のキー(仮想キーコード)MBAのキー入力
Backspace (0x08)delete
Pause (0x13)fn + esc
無変換 (0x1D)英数
ひらがなかな
Insert (0x2D)fn + enter
Delete (0x2E)fn + delete
左Win (0x5B)左command
右Win (0x5C)右command
IME ON/OFFcaps
PageUp (0x21)fn + ↑
PageDown (0x22)fn + ↓
Home (0x24)fn + ←
End (0x23)fn + →
PrintScreen (0x2C)fn + 左shift + F11
ScrollLock (0x91)fn + 左shift + F12
Ctrl + Alt + Deletefn + control + alt + delete

キーレイアウトの変更

Windows環境の場合、以下のレジストリを変更することである程度キーレイアウトを変更することができます。

\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layout\Scancode Map

ChangeKeyというフリーソフトでも変更可能です。このアプリが中でやっていることはレジストリの変更なので「管理者として実行」で起動する必要あり。

自分の場合、普段使っているのが91キーボードなので、こんな風にレイアウトしてみました。

  • 「Altキー」 → 「左Winキー」(交換)
  • 「左Winキー」 → 「Altキー」(交換)
    • キートップの表示とはずれますが、Altの位置が違うとアクセラレータの入力に戸惑うので位置を交換
  • 「右Winキー」 → 「アプリケーションキー
    • アプリケーションキー(右クリック同等)を入力する方法がShift + F10*1しかなかったので、使わない右Winキーを潰して割り当て

IMEの設定変更

Macでは標準になっている「かな」/「英数」でIMEのON/OFFが切り替わるのが好きなのでMicrosoft IMEも設定変更

*1:あまり使われていなそうだが、これはWindowsの標準機能

Googleで一文字検索してみた。(2011年版)

ふと思いついたので2008年にやった一文字検索を再度動かしてみた。

結果

検索文字1位の結果(2008年)1位の結果(2011年)
aA@A@
bトップページ|Bフレッツ|フレッツ公式|NTT東日本B - Wikipedia
cC言語 - Wikipedia[ C ] OFFICIAL SITE
dD official web siteD official website
ee+(イープラス)チケット|チケット情報・販売・購入・予約e+(イープラス)チケット|チケット情報・販売・購入・予約
fマクロスFRONTIERFリーグ公式サイト|Fリーグ2010powered by ウイダーinゼリー
gGポイントはポイント交換、ショッピングでおトクにポイントがたまる ...ポイント交換のGポイント。安心・便利・おとくにネットショッピングが ...
hYouTube - みなみけ ちょっとHなシーン詰め合わせ [ from ...YouTube - お嬢様はHがお好き Ojousama wa H ga Osuk
iタウン検索なら iタウンページi(アイ)| 軽自動車 | カーラインアップ | MITSUBISHI MOTORS JAPAN
jスラッシュドット・ジャパン: アレゲなニュースと雑談サイトJリーグ公式サイト
kK Official web siteK Official web site
l『L change the WorLd』公式サイト『L change the WorLd』公式サイト
mYouTube - PrincessPrincess - Mテレビ朝日|ミュージックステーション
nN - Wikipedia
o人生オワタ - WikipediaO - Wikipedia
pP-WORLD 全国パチンコ-パチスロ機種情報P-WORLD 全国パチンコ-パチスロ機種情報
qQ - Wikipedia* Q 「kju:」
rR による統計処理R-Tips
sアットエス 静岡県の「いま」がよ〜く見えるat-s | @S[アットエス]つなぐ!楽しむ!しずおかライフ | 静岡新聞SBS
tTポイントとTカードの総合サイト[T-SITE]TポイントとTカードの総合サイト[T-SITE]
uU - WikipediaU - Wikipedia
vVジャンプ WEBバレーボール Vリーグ オフィシャルサイト
wW - Wikipediaテレビ朝日|仮面ライダーW(ダブル)
xX JAPANプレミアムDVD販売開始 X JAPAN WORLD TOUR Live
yYahoo!オークションYahoo! JAPAN
zZ会日産:フェアレディZ [ Z ] スポーツ&スペシャリティ/SUV Webカタログ ...

雑感

  • 3年前のスクリプトがそのままでも動くGoogleすごい。class="l"とか1文字なので心配していたけど大丈夫だった
  • cinwz辺りを見ると単一文字の商品名でも一位をとれるみたい。SEOの成果?
  • hはやっぱりジャンル的には変わらないらしい
  • sは引き続き静岡
  • yがYahooオークションからYahooJapanへ
  • 人生オワタの一位オワタ\(^o^)/

ソース

require 'rubygems'
require 'hpricot'
require 'open-uri'
require 'jcode'

query = "abcdefghijklmnopqrstuvwxyz";
query.each_char do |letter|
	url = "http://www.google.co.jp/search?q=#{letter}&lr=lang_ja"
	doc = Hpricot(open(url).read)
	result = (doc./"//h3.r/a.l")[0]
	
	puts "<tr><td>#{letter}</td><td><a href=\"#{result[:href]}\">#{result.inner_text}</a></td></tr>"
end

素のままでも動いたけど、Youtubeの別枠扱いをなくすためにXPathを変更
今ならhpricotではなくNokogiriとかかなぁ。

Ruby製バウリンガル

エイプリルフールも終わったのでわんわんをデコードするRubyバウリンガルを載せてみる。

$KCODE = 'SJIS'

require 'zlib'

decode_table = {
	'わん' => 0,
	'きゃん' => 1,
	'わおん' => 2,
	'わーん' => 3,
	'ばう' => 4,
	'きゃうん' => 5,
	'わう' => 6,
	'きゅーん' => 7
}

encoded_text = ARGF.readlines.join.gsub(/[\r\n。!]/, "")
regexp = Regexp.new(decode_table.keys.join("|"))

octets = encoded_text.scan(regexp).map {|octet| decode_table[octet]}
compressed_text = octets.each_slice(3).map {|octet| octet.join.to_i(8) }.pack("C*")

$>.write(Zlib::Inflate.inflate(compressed_text))

東京国際アニメフェアのチラシをCanvas使って編集してみる

Twitterを見ていたらRTでこんなのが流れてきたわけですよ。



最近Canvas使ってないなー。と思ったのでカリカリとBookmarklet書いてみました。

Hatena::Letで参照

使い方

Firefox3.6.12とChrome8.0.552.215で動作確認。

1. Hatena::Letを開きます

2. 「bookmarklet:東京国際アニメフェアの開催告知チラシから参加拒...」をドラッグしてお気に入りに放り込みます。

3. 公式チラシ | 東京国際アニメフェア2011を開きます

4. 2番で追加したお気に入りを選択するとこんな感じになります。


しかし、最近の作品がわからない・・・

2010/12/11 01:58追記:
http://twitpic.com/3elscd にすごく分かりやすい画像があったので使わせてもらいました。

金星探査機あかつきの現状まとめ

12月7日22時の記者会見のまとめが主、手元にまとめたテキストをコピペ

時間のズレとかあるかもー

08時49分        軌道制御エンジン噴射開始
08時50分43秒    通信断絶開始(予定通り)
09時01分00秒    軌道制御エンジン噴射終了
09時12分03秒    通信再開予定だったが通信再開せず
10時03分        探査機側で通信アンテナをLGAに自動切り替え
10時28分        衛星のLGAからの通信が受信される
                MGAに切り替えるよう、衛星にコマンドを送信
                コマンドは送信でき、MGAに切り替えが出来た
10時59分        MGA通信確立できず
11時29分        通信確立できなかったので、衛星側でLGAに自動切換
14時00分        スペインマドリッド局にて補足、LGAから受信、強さ変化確認
                0.1rpm(10分で1周の周期)で回転していることが確認される
                セーフホールドモードと考えられるがこの時点では未確定
                MGAに切り替えるように地上から地上からのコマンド
                10分に1回、40秒受信可能
17時00分        回転停止コマンドを送信したが停止せず、セーフホールドモード継続
                この際止まらなかったのはコマンドが衛星に届いていないと推定されている
                衛星の軌道が本来と違う為に、アンテナがズレていた
21時00分        スペインマドリッド、アンテナ方向予測して修正
                MGAでも通信できたが安定しない為、LGAで通信してテレメトリデータを受信中

現状:
    セーフホールドモードに入っているのは確定
    LGAにてテレメトリの受信(22時〜24時ぐらい)
    軌道は不明

トラブルなければ明日の昼までに採れるはずの情報
    HouseKeeping情報は分かる
    軌道決定もできる・・と良いな


参考:
    地上の光回線    100000000 bps(100Mbps)
    HGA                256000 bps(256kbps)?
    MGA                   512 bps
    LGA                     8 bps

Amazon EC2/S3/etc.の無料サービスに登録してみた

Amazonが11月1日から機能限定での無料サービスを始めたので、遅ればせながら試しに登録してみた。

参考:Amazon EC2、1年間限定でフリーミアムサービスを開始。日本でも利用可能

AWSアカウントを登録

1. 公式ページ(http://aws.amazon.com/free/)のSign Up Nowボタンから登録ページへ
2. メールアドレス、氏名、パスワード、住所、国、電話番号辺りを登録
注意:郵便番号にハイフンが無い場合、Invalid Addressといわれて受け付けられない
3. 登録完了

EC2の利用申請

EC2のページ(http://aws.amazon.com/jp/ec2/)からAmazon EC2の利用を申し込む
Free tier for new AWS customersとして条件が書かれていたり

アジア地域の場合はMicroInstanceで0.025$/hour。
普通に使った場合はこんな感じ

  • Amazon EC2 Linux/Unix Micro Instance
    • 0.025($) * 80(円/$) * 24(時間) * 31(日) = 1488円ぐらい
  • Elastic Load Balancing
    • 0.028($) * 80(円/$) * 24(時間) * 31(日) = 1666円ぐらい
    • 0.008($) * 80(円/$) * 15(GB) = 9.6円ぐらい
  • Amazon Elastic Block Storage(EBS)
    • 0.11($) * 80(円/$) * 10(GB) = 88円ぐらい
    • 0.11($) * 80(円/$) * 1(million IOs) = 8.8円ぐらい
    • 0.15($) * 80(円/$) * 1(GBSnapshot) = 12円ぐらい
    • 0.01($) * 80(円/$) * 1(x1000 PUT)= 0.8円ぐらい
    • 0.01($) * 80(円/$) * 1(x10000 GET)= 0.8円ぐらい

後半すごく細かいが、合計すると3274円ぐらいか。といってもぎりぎりまで使った場合だけど。
他にも5GBまでのS3が無料だったり、色々とお得な様子。


MicroInstanceの範囲であればインスタンス代は無料なわけで、掛かるとしたらデータ転送料ぐらいか。
15GBまでは無料。それを超えると1GBに付き12円ぐらいなので100GBで1200円。
まぁ、まずは帯域を使うようなサービスを作らないと意味が無い計算だなぁ。


と、説明を見たらクレジットカード番号を入力して次ページへ。


電話番号を入力するとPINが表示され、同時に電話が掛かってきます。
当然のように英語の自動音声が流れますが気にせず携帯からPINを入力。
承認されるとWebページが自動的に切り替わります。フィードバックはえぇ。


次ページに進むと、再度色々なサービスの値段が表示される。とりあえず無視してComplete Sign Up

AWS Managing Console

WebベースでEC2のインスタンスを管理できるっぽい。

  1. まずはRegionをASIAに切り替え。
  2. Service HealthがAPAC - Singaporeになっているのを確認してLaunch Instance。
  3. Community AMIsを見るとUbuntuやらCentOSやら見えるけど、とりあえずはQuick Startで32bit Amazon Linux AMIを起動してみる
  4. Instance数は1個、Instance TypeからMicroを選択してLaunch Instances
  5. Advanced Instance OptionsでkernelやらRAMディスクやら選べるみたいだけどとりあえずそのままContinue
  6. Tag付けも別に不要なのでそのままContinue
  7. 鍵の作成。名前を入れると秘密鍵を生成してダウンロードしてくれる。同時にインスタンスのauthorized_keysに追加されてるのかな。
  8. SecurityGroupの作成。後で変えれば良いのでSSHだけ許可
  9. 設定内容が表示されるので確認してLaunch

10秒とかからずインスタンスが立ち上がる。
とりあえずCloseでウィンドウを閉じて見に行ってみる。


左メニューのInstancesから一覧を表示すると今作ったインスタンスが動いている。
Instance ActionからConnectを選ぶとSSHでの接続方法が表示される。

後は適当にいじってみる

Puttyの場合、先にキーの変換をしないとなのでputtygen.exeを起動して変換
ppkファイルが出来たので説明にしたがって接続・・・
rootユーザじゃなくてec2-userでログインしてね。と切断された。・・・なぜWebの説明でrootユーザにしたし。


気を取り直してec2-userで再挑戦するとログイン成功。
アスキーアートでエンボス加工されたEC2の3文字が。目を細めると良く見える。

・とりあえずuname

[ec2-user@ip-10-130-59-18 ~]$ uname -a
Linux ip-10-130-59-18 2.6.34.7-56.40.amzn1.i686 #1 SMP Fri Oct 22 18:48:33 UTC 2010 i686 i686 i386 GNU/Linux

・sudo権限もある

[ec2-user@ip-10-130-59-18 ~]$ sudo echo "hoge"
hoge

iptablesはからだった。てことはSecurityGroupでのSSH許可はインスタンスより上のレベルで制御されてるのか。


・ディスクは9Gぐらい空きあるけど、永続データはS3におくべきなんだっけ?
試しにファイルを作って再起動したら普通に残っていた。まぁ、プログラムとか毎回消えたらdeploy面倒だよね。


VIMは7.0.237だった。
.vimrcは全くなかったのでいつものをコピペ。
でもよくあることでambiwidth=doubleにしていても記号がおかしかったり。しょぼん。