2007年 12月 08日

Deferred チェインのときの setTimeout

http://coderepos.org/share/browser/lang/javascript/jsdeferred/trunk/jsdeferred.js?rev=2761#L119

の setTimeout について、スタックを消費するからこうしている、と書いたのは、next の問題で無駄な Deferred を生成していたせい ( http://subtech.g.hatena.ne.jp/cho45/20071202/1196571302 ) で、Stack over flow がでたことがあったからなんだけど、next の問題は解決したので、あらためて必要かどうか考えたらいらない気がした。

でもって http://coderepos.org/share/changeset/2817 はずしてみた。Safari はスタックサイズがかなりちっちゃくて、600?くらいの再帰でオーバーフローするからテストでは loop(1000 を実行してちゃんと戻ってくるかをテストしてる (loop は内部で next と call よんでる)。

この修正で、チェイン間ではブラウザへ処理がもどらなくなる。もどしたいなら wait(0) を return すればもどせる。

Stack over flow

Mac 10.4.11 Mem 2G

javascript:n=0;(function(){n++;arguments.callee();})();
javascript:alert(n);
Firefox 3.0b1
261503
Safari 3.0.4
500
Opera 9.24
3340
Thunderbird 2.0.0.9
1000

Fx の深さはなんなんだ……

Rhino だとインタプリタモードのときはいくらでもいける……

$ rhino -opt -1 -e 'n=0;(function(){n++;print(n);arguments.callee();})();'
#=> どこまでも
$ rhino -opt 0 -e 'n=0;(function(){n++;print(n);arguments.callee();})();'
2054
/home/cho45/bin/rhino: line 11: 31589 Segmentation fault      "$JAVA" -jar ~/bin/js.jar "$@"
exit 139

あふれると SEGV ってのはどうなんだ

2007年 12月 07日

Rhino でテスト

いろつき!

ブラウザの機能使わないテストだと rake でテストできていいなぁ

JS のドキュメンテーションツール

JSDoc Toolkit がよさそうなんだけど、どっちにしても Java Doc 形式のコメントが好きになれないので使う気がおきなかった……(ここでまた慣れるか、オレオレ文法でいくかで悩む。でも @return とか @param とかすごいダサく感じる……)

MochiKit は python の docutil (?) とかいうのをつかってるみたいだ。スクリプトのほうには id だけつけて、rst ? っていうのにドキュメントがまとまってる。よくわかんない。

あと MochiKit はスクリプトの圧縮に rhino をつかってるっぽい (custom_rhino.jar/js.jar) けど詳細がよくわからない。Rhino/Spidermonkey は JS 内部から JS トークナイザにアクセス (Ripper みたいに) できたらいいのに……Rhino のほうは内部メソッド叩いたらできるんかなぁ。


それとドキュメントをソースコードに書くべきかどうかがいまいち迷う。RDoc はソースコードにまぜて書くけど、見通しが悪くなるんだよなぁ。でも最低限の使いかたはコメントとして近くに書いておきたいし、挙動を変更したらすぐ書きかえたいから、近くにあるのは便利だ。

あとドキュメントに実装を簡単に見れるしくみが絶対必要だと思う。長い英語よむぐらいならソース見たほうが理解できるし、ソース見てからドキュメント読むとよめるようになるし……

Twisted

正直にいうとごくごく最近まで MochiKit Deferred の由来が Twisted (Pythonの非同期ライブラリ) からきていると知らなかった……
Python のマルチスレッドってどうなってるんだろう。threading/thread とかがヒットする。

Ruby のスレッドはなんか頭つかわなくても書けるなぁ。なんでだろう……あと Ruby で Deferred は必要か、っていうのが微妙に最近ひっかかる。なんで Ruby には Deferred がないんだろう (つまり、なんでなくてもいいやって思えるんだろう)

JS だとスレッドがなくて、しかもいちいちブラウザに処理をもどさないと固まってしまうから Deferred がすごい威力を発揮するけど、スレッドがあったときに、Deferred はどう活躍するんだろう。(Python にはスレッドがあるっぽいけど、なんで Deferred が必要になったんだろう)

Ruby だと非同期な HTTP GET をやるなら

require "open-uri" # net/http はめんどいから

t = Thread.start do
	open("http://example.com/") {|f|
		f.read
	}
end

p t.value

みたいになって、複数あつめてやるのをスレッドつかってかくと

require "open-uri"

t1 = Thread.start do
	open("http://example.com/") {|f|
		f.read
	}
end

t2 = Thread.start do
	open("http://example.com/") {|f|
		f.read
	}
end


[t1.value, t2.value].each do |i|
	p i.length
end

さらにつなげていこうとすると大変? そうでもない気がする。うーん

最初から非同期なやつだと (ぱっとでてこない)、めんどいかなぁ……

いやいやそもそも Deferred は非同期なやつをほげほげするやつなんだ。

Ruby だと非同期なメソッドがあんまりないから気にならないのかなぁ。

Greasemonkey スクリプトの構成

なんでこんなふうに書いてるの? って感があるのかもしれないから、いちおうかいとくよ!

(function () { // なんか GM ではいらなくなってるっぽいけど、一応かこう

// メインの処理
foobar...


// ユーティリティ関数たち
function $X () {
}


})();

って感じで書いてる。メインの処理、その GM で一番重要なところをできるだけ上に書きたい (ただし、うえから順に読んでも理解できるように)。なのでユーティリティ関数とかは関数式ではなく関数宣言で書く。宣言で書いた場合はあとに書いても前のほうで参照できる。

foo();

function foo () {
    foobar();

    function foobar () {
    }
}

// baz(); //=> これはできない

var baz = function () {
};

baz();

でも、これだと prototype への代入とかをするのをうしろに書けない。そういうときは、

with (F()) {
    var baz = new Foo();
}

function F () {
    function Foo () {
        return (this instanceof Foo) ? this.init() : new Foo();
    }
    Foo.prototype = {
        init : function () { }
    };

    var foo = {};
    foo.Foo = Foo;
    return foo;
}

とかやるとうまいこと分離できるぽい。

JSDeferred の jsdeferred.userscript.js は、コピペ用のコード ( http://svn.coderepos.org/share/lang/javascript/jsdeferred/trunk/jsdeferred.userscript.js ) を生成していて、これを最後にコピペして with をつかえば綺麗にかけるようにようにした。

with (D()) {
    next();
}

// コピペ
function D () {

}
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;
}