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日

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 自身で書かれているから、あきらかにおかしいときはテストの数があっているかどうかをみる)

2007年 12月 01日

jQuery Deferred

http://svn.coderepos.org/share/lang/javascript/jquery-deferred/sample.html
http://jquery.com/plugins/project/deferred

いろいろサンプル書いてつかいみちを考えているとたのしい。

var list = [];
list.push($.get("a.html").next(function (data) { return data.match(/<title>([^<]+)<\/title>/) } ));
list.push($.get("b.txt" ).next(function (data) { return data.match(/##\s*([^<]+)\s*##/) } ));
list.push($.getJSON("c.json").next(function (data) { return data.title } ));

parallel(list).next(function (values) {
	print("Titles "+values.join(", "));
});

みたい書ける。たのしい。(うえのコードがうごくかはためしてない)

それぞれ、べつべつのフォーマットのリソースをとってくる。タイトルを抽出するところまでは個々の Deferred が処理して、そいつらを parallel (DeferredList) であつめてくる。

でもそういうふつうの (謎) 非同期処理より、ループの分割のほうがおもしろい。

loop({begin: 1, end:100, step:10}, function (n, o) {
	print(["Processing devided loop:n=", n].join(""));
	for (var i = 0; i < o.step; i++) {
		var j = n + i;
		print(j);
	}
});

function print (m) { $("#loop-code1").append("\n// "+m) }

1 から 100 までをループする。ただし最大 10 個に制限して分割してループする (o.step を条件にしているのは、境界付近でよしなにするため。最後 (o.last == true) 以外は常に渡した step と一緒)。定期的にブラウザに処理がもどるので固まったようにならない (100 ぐらいじゃ固まらないけど)。固まりさえしなければある程度時間がかかってもストレスがたまらないから嬉しい。

ディレイいれながらループも簡単にかける

loop(5, function (i, o) {
	print(i);
	return o.last? i : wait(1);
}).
next(function (e) {
	print("end ["+e+"]");
});

1秒ずつまちながら 5 回ループする (0,1,2,3,4)

最後の値を次へ渡したいので条件をいれてる。end [4] と最後にでる。

2007年 11月 30日

Ruby1.9 のシンボルキーハッシュリテラルの簡易記法

p({ foo: "abc", bar: "def" })

こうかけるやつなんだけど、JS みたいなノリで

p({ foo : "abc", bar : "def" })

と書くとパースエラーになる。


シンボルキーと他のキーは同時に書けるけど、ちょっと混乱するかもしれない

p({ foo: "abc", bar: "def", "baaa" => "aaa" })

それに、うえのように書いても "baaa" と :baaa は違うオブジェクトなので

p foo[:baaa]  #=> nil
p foo["baaa"] #=> "aaa"

Ruby で arguments.callee ってどうやるんだろ

self == -> { self }.()

(もちろんまちがってないけど) true だしなぁ。

引数かえられないけど redo で現在のメソッドを最初からやりなおすのはできる……

Binding#callee とか実装できないかなぁ (実行中の Method/Proc をかえす)

jAutoPagerize / はてなスター

フィルタの適用方法を変えて、importNode されたノード個々に対してフィルタを適用するようにした。本家と挙動が違うかもしれない。(フィルタ書いたことなくてよくわからない)

でもってはてなスターがロードされているページではデフォでスターをロードするようにした。Hatena.Star に特定ノード以下に制限してロードするのがあったのでそれつかってみた。(はてダのクイックページャのソースからたどった)

GM の config

直書きだと、バージョンアップするたびにコピペが発生してめんどい。どうにかしたい。

最近のまいぶーむ -> MochiKit Deferred の再発明

MochiKit の Deferred がよくできてるなぁと実感する。Deferred チェイン中に Deferred return するとチェイン中断して……みたいなのがかっこいい (そういうのを Deferred 一個で統一してできるようにしているので MochiKit Deferred は結構サイズが大きい)。でも API がどうしても好きになれない。jQuery に移植したい。

でも慣れないとよくわからないよなぁ……(setTimeout の挙動 (ブラウザの実行キューのほげほげ) とかをわかってないと、Deferred つくってるところで setTimeout(, 0) してて、なにこれ? っておもうことになる。)

Deferred チェイン。

MochiKit の実装はもっとシンプルにできそうなのでつくってみた。でもいろいろ頭悪くて考えきれてないと思う……

例えばこういう風に書けるようにする。

wait(1).
next(function (e) {
	log([1, e]);
	return wait(1).next(function (e) {
		log([2, e]);
		return wait(2).next(function (e) {
			log([3, e]);
		});
	});
}).
next(function (e) {
	log(4);
	return "555";
}).
next(function (e) {
	log(e);
})

末尾再帰っぽいの (return call ってのが tailcall っぽくてよくないですか><)

next(function () {
	log("start");
}).
next(function () {
	function pow (x, n) {
		function _pow (n, r) {
			if (n == 0) return r;
			return call(_pow, n - 1, x * r);
		}
		return call(_pow, n, 1);
	}
	return call(pow, 2, 10);
}).
next(function (r) {
	log([r, "end"]);
}).
error(function (e) {
	alert(e);
})

実装

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

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

	cancel : function () {
		this._next = null;
	},

	_post : function (okng, fun) {
		this.callback[okng] = fun;
		this._next = new Deferred();
		return this._next;
	},

	_fire : function (okng, value) {
		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);
		}
	}
};

function wait(n) {
	var d = new Deferred();
	var t = new Date();
	setTimeout(function () {
		// 実際にかかった時間をコールバック
		d.call((new Date).getTime() - t.getTime());
	}, n * 1000)
	return d;
}

function ps () {
	var d = new Deferred();
	setTimeout(function () { d.call() }, 0);
	return d;
}

function next (fun) {
	return ps().next(fun);
}

function call (f, args) {
	args = Array.prototype.slice.call(arguments);
	f    = args.shift();
	return next(function () {
		return f.apply(null, args);
	});
}

なかなかカッコいい感じがする。もうちょいいじる。

next は this ではなくて、次の Deferred (this が準備できたらよばれる Deferred // _next) をかえす。これでずっとチェインしていく。もしコールバックが Deferred をかえしたら、その Deferred に _next をわたしてあげる。_next は継続 (ていっていいのかな。継続をちゃんと理解していない)

スタック消耗してたのを修正した

Deferred loop

function loop (o, fun) {
	var begin = o.begin || 0;
	var end   = o.end;
	var step  = o.step || 1;
	var ret;
	return next(function () {
		function _loop (i) {
			if (i < end) {
				ret = fun.call(this, i, step);
				return call(_loop, i + step);
			} else {
				return ret;
			}
		}
		return call(_loop, begin);
	});
}


next(function () {
	var hoge = 0;
	return loop({end:50000, step:50}, function (n, step) {
		for (var i = 0; i < step; i++) {
			hoge += n+i;
		}
		return hoge;
	});
}).
next(function (e) {
	log(e);
	log("end");
}).
error(function (e) {
});

50000回のループを50ずつ実行する。