2007年 12月 06日

jQuery Deferred

名前がやっぱよくないよなぁ。General Deferred とかにすればいいのかなぁ (jQuery 部分は分離して Rakefile で一括生成)。

  • JSDeferred
  • General Deferred
  • jKuery Deferred
  • JSContinuation (Deferred って名前がウケ悪い説 でもおかしいからだめだな)

JSDeferred にしよう。作業かいし

http://svn.coderepos.org/share/lang/javascript/jsdeferred/trunk/

mini (簡易圧縮バージョン) と nodoc (コメント全削除) もコミットするようにした。rake すれば生成される。mini と nodoc は見た目の差の割にサイズがそんなに変わらない (タブインデントだし) ので nodoc だけでもいい気もする。

Rhino memo

なんか、Rhino 上で setTimeout エミュレートしてテストしようとおもったら、そもそも言語的な部分の非互換を踏んでだめすぎる……

$ rhino
js> a = undefined || 1
true <= expect 1
js> a
true
js> a = undefined || 0
js> uneval(a) // (undefined は表示されないっぽいので uneval)
undefined <= expect 0
js> undefined instanceof Object
js: "<stdin>", line 1: The undefined value has no properties. <= expect false

あと Rhino のバージョンってどうやってみるんだろ。apt-cache show rhino すると

Version: 1.6.R1-0.0ubuntu3

だけど、これってめっちゃ古くて実は上の問題は新しいのだと解決していたりするのかな

setTimeout のエミュレートはよくわからんけど spawn と sync つかってかいた。けど、べつに使わなくていいよなと今読みなおしておもった……

http://coderepos.org/share/browser/lang/javascript/jsdeferred/trunk/test-rhino.js?rev=2673#L94

最新にしたらいけた……sudo apt-get remove rhino

$ wget ftp://ftp.mozilla.org/pub/mozilla.org/js/rhino1_6R7.zip
$ unzip rhino1_6R7.zip
$ cp js.jar ~/bin
$ cd ~/bin
$ cat rhino
#!/bin/sh

if [ -z "$JAVA" ]; then
  if [ -n "$JAVA_HOME" ]; then
    JAVA="$JAVA_HOME/bin/java"
  else
    JAVA=/usr/bin/java
  fi
fi

"$JAVA" -jar ~/bin/js.jar "$@"
2007年 12月 05日

JavaScript のコンストラクタ関数

new つけてもつけなくてもいいようにするにはどう書くのがベストか的なんだろう。Deferred ではこうしたけど

function Deferred () { return (this instanceof Deferred) ? this.init(this) : new Deferred() }
// prototype.init では this をかえしてる。
// でもコンストラクタは Object をかえした場合にしかその返り値を使わないので undefined でもべつにいい。

汎用的にするなら

function Foo () {
    if (this instanceof arguments.callee)
       this.init(this);
    else
       return new arguments.callee;
}

かな。ただ、これだと任意の数の引数がとれない (new は括弧をつけなくていいことからも解るように関数呼びだしではないので)。

function Foo () {
    if (this instanceof arguments.callee) {
       this.init.apply(this, arguments);
    } else {
       // new のエミュレート
       var f = function () {};
       f.prototype = arguments.callee.prototype;
       var o = new f;
       arguments.callee.apply(o, arguments);
       return o;
    }
}

どうみても素直に引数リストを書いたほうがよみやすくていい。(instanceof (がつかっている [[HasInstance]]) は [[Prototype]] をみる。new 以外に [[Prototype]] をセットする方法はない。)

てかはてなで

[[Prototype]]

[] [[Prototype]] [] <- リンクはされないけど括弧が消える。

を地で書くにはどうしたらいいんだ……

ちなみに global ( (function () { return this })() ) と、this をくらべるのはよくないことがある。

jQuery Deferred だと $.deferred = Deferred とやっているわけだけど、この場合 this は $ (jQuery) になってしまうのでだめ。

Deferred

jQuery Deferred とか言っていますがコアは全く jQuery に依存しないので (いい名前がおもいつかないから jQuery バインディングのほうのなまえでよんでる) http://svn.coderepos.org/share/lang/javascript/jquery-deferred/jquery-deferred.js の Deferred 関数と必要な関数をコピペしたら GM でもうごきます。setTimeout/clearTimeout にだけ依存してるのでその実装があるならどこでもうごくはず。

他の非同期なやつを Deferred 化するのは jQuery Deferred だと

function wait (n) {
	var d = new Deferred(), t = new Date();
	var id = setTimeout(function () {
		clearTimeout(id);
		d.call((new Date).getTime() - t.getTime());
	}, n * 1000)
	d.canceller   = function () { try { clearTimeout(id) } catch (e) {} };
	return d;
}

こんな感じになってます。Deferred#call がコールバック起動で、Deferred#fail がエラーバック起動です。

MochiKit のコードだと (機能がちょっと違いますが)

    /** @id MochiKit.Async.wait */
    wait: function (seconds, /* optional */value) {
        var d = new MochiKit.Async.Deferred();
        var m = MochiKit.Base;
        if (typeof(value) != 'undefined') {
            d.addCallback(function () { return value; });
        }
        var timeout = setTimeout(
            m.bind("callback", d),
            Math.floor(seconds * 1000));
        d.canceller = function () {
            try {
                clearTimeout(timeout);
            } catch (e) {
                // pass
            }
        };
        return d;
    },

引数 (value) をわたすためにチェインつかってるのがおもしろいすね。


ついでに jAutoPagerize でつかっているやつ。

function CachedResource (uri, convertfun, expire) {
	var d   = Deferred(); // new なしでいける。
	var key = uri;
	var v   = {};
	try { v = eval(GM_getValue(key)) || ({}) } catch (e) { log("parse error: may be uneval bug") }
	d.clear = function () {
		GM_setValue(key, "");
		return this;
	};
	if (v.time && v.time > (new Date).getTime() - expire) {
		log("Cache Hitted: " + key);
		setTimeout(function () { d.call(v.body); }, 10);
	} else {
		log("Cache expired; getting... " + key);
		GM_xmlhttpRequest({
			method  : "GET",
			url     : uri,
			onload  : function (req) { try {
				var res = convertfun(req.responseText);
				GM_setValue(key, uneval({time:(new Date).getTime(), body:res}));
				log(key, uneval({time:(new Date).getTime(), body:res}));
				log("Cached: " + key);
				d.call(res);
			} catch (e) { d.fail(e) } },
			onerror : function (e) {
				d.fail("HTTPError:"+e);
			}
		});
	}
	return d;
}
2007年 12月 04日

Deferred による疑似マルチスレッド

Deferred をつかってマルチスレッドっぽいこともできます。

loop(10, function (n) {
	print(n);
	return wait(0.1);
});

loop(10, function (n) {
	print(String.fromCharCode(97+n));
	return wait(0.2);
});

このように書くと、0 から 9 までの数値と a から j までの文字がまざって出力されます。

例: 0 a 1 b 2 3 c 4 5 d 6 7 e 8 9 f g h i j

loop 関数は Deferred をかえすので、うえのふたつが終ったら実行する処理を書くなら、

parallel([
	loop(10, function (n) {
		print(n);
	}),
	loop(10, function (n) {
		print(String.fromCharCode(97+n));
	})
]).next(function () {
	print("End");
});

とか書けます (MochiKit Deferred でも書けるはずだけどめんどいからためしてない)。Deferred のおもしろいところは、めちゃくちゃ簡単で Greasemonkey スクリプトにコピペして使えるぐらいの長さのコードでこういうことができることだと思います。

2007年 12月 02日

生姜

生姜の匂いを「冬を感じる優しいかおり」と表現しているのを見てなんかぐっときた。

Good Dog Happy Men - the GOLDENBELLCITY -嬉しくて悲しいこと-

3部作の完結のフルアルバムがでた。数日聴きこんでみたけど、「Groria Street から愛を込めて#3 」(原文ママ) が素晴らしい。「ただ生きるよりルール」

「Apple star storyS」 はヘッドフォンで聴くと面白い。ライブ音源もいいけどこれも好き。Most beautiful in the world にはASの歌詞がついてなくて、聴きとってテキストにしといたのだけど、何いってのかよくわかんなくてなんとなくで書いたりしたところがあった。でも答えあわせをしたら割とあってた (鍵カッコつけた部分まであってて笑った)。(「星空」を「この空」とまちがえてた (シャウトしててききとれない)。「たまにわからなくなるけど」を「たまにわからなくなるねと」とまちがえてた (なんで間違えたのかわからないけどw))

「そして列車は行く」はなんか長い部分を見ていろいろカッコいい。うまくいえない。曲としてめっちゃカッコいいのもそうだし、「今 拍手喝采とか涙よりもっと」とか、いちいちかっこいい。

「記憶と記録」は四人のゴブリン大いに踊るに収録されていた「微笑とメロディー」のリアレンジなんだけど、このアレンジがなんか想像の斜め上をいきまくってておもしろい。

でもって「黄金の鐘」なんだけど、最初の歌詞から大分歌詞が変わってて残念なところがあったりするけど、聴いてるとどうでもよくなってきた。これはたぶん聴きこむともっとじわじわきそうだなぁ。

ツインドラムがかっこいいからライブでききたいなぁ……

the GOLDENBELLCITY

MochiKit Deferred と jQuery Deferred の違い

(動いてはいるけど、ちょっと挙動が思ったとおりでない気がする (巨大なループのあと、次のプロセスへ進むのが遅い気がする) のでコードを考えなおしのために現在の実装をメモ書きします) なんか jQuery Deferred って書くと jQuery Core に Deferred システムがあるみたいにみえるけどちがうよちがうよ。でも内部的には animate あたりで必要だから持ってるんだとおもう……

チェインの根本的構造

MochiKit Deferred では子と親がはっきりわかれていて、子をつくるときには親を pause し、子が実行しおわったら親の pause を解除するようになっています (だと思うけど、実はあんまり使ったことなくてわからない)。これは、MochiKit の Deferred が Array でチェインを持って処理をしていて、子 (もまた Array でチェインをもっている) がかえされたとき、こうするのが一番だからだと思います。

jQuery Deferred では子も親もはっきりわかれておらず、子 Deferred が出現したら、現在の継続を子 Deferred の継続にし、親はもう過去の存在となるような実装にしています (親には戻らない)。そういうアレで pause がないです。

jQuery Deferred は一個の Deferred は一個のコールバックしか持ちません。処理のプロセス一つをパッケージングし、次のプロセス (を Deferred でパッケージしたもの=継続) を持っています。Deferred.prototype.next(fun) は fun を this のプロセスとし、さらに新しく Deferred をつくり、新しく Deferred を作り、そのプロセスを fun とし、それを this の継続として設定する関数です。

// global function next
next(function () {
	console.log(["chain", 1]);
}).
// Deferred.prototype.next
next(function () {
	console.log(["chain", 2]);
});

// 定義ずみの関数
/*
function next (fun) {
        // 新しく呼ばれることが約束された Deferred をつくる
	var d = new Deferred();
	setTimeout(function () { d.call() }, 0);
        // fun をその Deferred のプロセスとして設定し返す。   
	d.callback.ok = fun;
	return d;
}
*/

うえのコードは

// あとで呼ばれることが約束された Deferred を作成
d1 = next(function () {
	console.log(["chain", 1]);
});

// 次のプロセスをパッケージする Deferred を作成
d2 = $.deferred(); // (Deferred をエクスポートしてないので new Deferred() とはできません。
// プロセスを設定
d2.callback.ok = function () {
	console.log(["chain", 2]);
};

// d1 の継続を d2 に設定
d1._next = d2;

と同じです。

子 Deferred の例をだしてみます。

next(function () {
	console.log(["child", 1]);
	return next(function () {
		console.log(["child", 2]);
	});
}).
next(function () {
	console.log(["child", 3]);
});

このように Deferred をコールバックで返すと、コールバックを実行した Deferred は返された Deferred の継続に自分の継続をセットし、自分ではなにもしません。

next(function () {
	console.log(["child", 1]);
	ret = next(function () {
		console.log(["child", 2]);
	});
	ret._next = this._next;
	this.cancel();
}).
next(function () {
	console.log(["child", 3]);
});

これと全く一緒です。

実装では以下のようになっています。

call  : function (val) { return this._fire("ok", val); },
fail  : function (err) { return this._fire("ng", err); },

_fire : function (okng, value) {
	// if (typeof log == 'function') log("_fire called");
	var self = this;
	var next = "ok";
	try {
		value = self.callback[okng].call(self, value);
	} catch (e) {
		next  = "ng";
		value = e;
	}
	if (value instanceof Deferred) {
		value._next = self._next;
	} else {
		setTimeout(function () {
			if (self._next) self._next._fire(next, value);
		}, 0);
	}
}

_fire が実際にコールバックしている関数です (この関数は call/fail から間接的に呼びます)。コールバックの返り値が Deferred のインスタンスの場合は、それの _next を設定しているだけです。それ以外の場合は setTimeout を通して継続をよびだしています (setTimeout をつかっているのは、永遠とコールバックチェインが続くとスタックオーバーフローになるからです)。

エラーの処理

jQuery Deferred では一つしかプロセスをもっていないと書きましたが、実際のところエラーを処理するためのコールバックも持っています。とりあえず例をだすと (テストケースから)

next(function () { throw "Error"; }).
error(function (e) {
	expect("Errorback called", "Error", e);
	return e; // エラーのリカバリー
}).
next(function (e) {
	// next だけど、リカバリーされたので実行される。
	expect("Callback called", "Error", e);
	// また投げてみる。
	throw "Error2";
}).
next(function (e) {
        // エラーがリカバリーされていないのでよばれない。
	ng("Must not be called!!");
}).
error(function (e) {
        // エラー専用のチェインをたどりここまでくる。
	expect("Errorback called", "Error2", e);
});

コールバックで発生したエラーはエラー専用のチェインをとおります。また、エラーバックでエラーを処理し、値をなげなおすことで、後続の処理を続けることができます。

このエラー専用のチェインですが、単に throw をくりかえすだけのチェインです。Deferred は以下のように初期化されれ、デフォルトのコールバックを持っています。

init : function () {
	this.callback = {
		ok: function (x) { return x },
		ng: function (x) { throw  x }
	};
	this._next    = null;
},

デフォルトでは ng は常に throw をするため、前途の _fire の catch に捉えられ、継続の ng を実行するように伝えられます。エラーのチェインへの分岐は throw するかしないかなので、エラーバックで普通に return すればエラーのリカバリーになります。



なんかつかめそうでつかめない。どっかおかしいような気がする。頭悪いのがむかつく

あーわかった。わかった。next で呼ばれる Deferred を一個余計につくっていたせいだった。

http://coderepos.org/share/changeset/2287

で修正した。ついでに説明も修正した。テスト書いといてよかった……役にたった (テストも Deferred 自身で書かれているから、あきらかにおかしいときはテストの数があっているかどうかをみる)