無限リストと遅延評価 ― 2008年02月02日 15時53分
IT 戦記で Haskell のリストを JavaScript で書くというのをやっている。これは面白い。ただ、そのまま書くと無限リストが無限再起に陥ってしまうので、遅延評価を行わなくてはいけない。
関数式を使った遅延評価
JavaScript で遅延評価を行うにはどうすればいいか。その答えのひとつが関数式だ。リストの各セルを、先頭の値と後続のリストという構造ではなく、先頭の値と後続のリストを返す関数という構造にしてやれば、リストの最初のセルを評価した時点で残りのセルがすべて評価されるという事態を防げる。
具体的には、リスト構築の際、後続のリストそのものの代わりにリストを返す関数を渡し、後続のリストを得るときは関数呼び出しを伴うようにすればよい。なお、ここでは空リストを表現するのに nil という特殊な値を用いる。nil は先頭の値も後続のリストも nil 自身であるリストである。
var _ = this;
_['[]'] = (function () {
var nil = [, function () { return nil; }];
nil[0] = nil;
return function () { return nil; };
})();
_[':'] = function (x) {
return function (xs) {
return [x, xs];
};
};
_['map'] = function (f) {
return function (x) {
if (x == _['[]']()) return _['[]']();
return _[':'](f(x[0]))(function () { return _['map'](f)(x[1]()); });
// function () ... リスト構築時の第 2 引数には関数を渡す。
// x[1]() 後続のリストを参照するときは関数呼び出しを行う。
};
};
_['!!'] = function (x) {
return function (i) {
if (x == _['[]']()) throw 'index too large';
if (i == 0) return x[0];
return _['!!'](x[1]())(_['-'](i)(1));
};
};
_['+'] = function (a) { return function (b) { return a + b; }; };
_['-'] = function (a) { return function (b) { return a - b; }; };
_['*'] = function (a) { return function (b) { return a * b; }; };
_['/'] = function (a) { return function (b) { return a / b; }; };
function stringifyList(list) {
if (list == _['[]']()) return '[]';
return '[' + list[0] + ', ' + stringifyList(list[1]()) + ']';
}
function take(list, n) {
if (n == 0) return [];
if (n == 1) return [list[0]];
return [list[0]].concat(take(list[1](), n - 1));
}
// a = []
_['a'] = function () { return _['[]'](); };
print(stringifyList(a()));
// => []
// b = [1]
// b = (:) 1 []
_['b'] = function () { return _[':'](1)(function () { return _['[]'](); }); };
print(stringifyList(b()));
// => [1, []]
// c = [1, 2]
// c = (:) 1 ((:) 2 [])
_['c'] = function () {
return _[':'](1)(function () {
return _[':'](2)(function () {
return _['[]']();
});
});
};
print(stringifyList(c()));
// [1, [2, []]]
// d = [1..10]
_['d'] = function () {
var ff = function (i) {
if (i == 0) return _['[]']();
return _[':'](1)(function () {
return _['map'](_['+'](1))(ff(_['-'](i)(1)));
});
};
return ff(10);
};
print(stringifyList(d()));
// => [1, [2, [3, [4, [5, [6, [7, [8, [9, [10, []]]]]]]]]]]
print(_['!!'](d())(2));
// => 3
// e = [1..]
_['e'] = function () {
return _[':'](1)(function () {
return _['map'](_['+'](1))(_['e']());
});
};
print(_['!!'](e())(2));
// => 3
print(take(e(), 5));
// => 1,2,3,4,5
// f = [1,3..]
_['f'] = function () {
return _[':'](1)(function () {
return _['map'](_['+'](_['-'](3)(1)))(_['f']());
});
};
print(take(f(), 5));
// => 1,3,5,7,9
// g = [ x * x | x <- f ]
_['g'] = function () {
return _['map'](function (x) { return _['*'](x)(x); })(_['f']());
};
print(take(g(), 5));
// => 1,9,25,49,81
式クロージャ
しかし、このままでは function やら return やらが並んでいていささか読みづらい。そこで JavaScript 1.8 から導入された式クロージャの出番である。これは、関数本体が return 文のみからなるとき、波括弧と return キーワードを省略できるという記法である。これを使って前述のリストを書き直すと以下のようになる。
var _ = this;
_['[]'] = let (nil = []) (nil[0] = nil)[1] = function () nil;
_[':'] = function (x) function (xs) [x, xs];
_['map'] =
function (f)
function (x)
(x == _['[]']()) ? _['[]']()
: _[':'](f(x[0]))(function () _['map'](f)(x[1]()));
_['!!'] = function (x) {
if (x == _['[]']()) throw 'index too large';
return function (i) (i == 0) ? x[0] : _['!!'](x[1]())(_['-'](i)(1));
};
_['+'] = function (a) function (b) a + b;
_['-'] = function (a) function (b) a - b;
_['*'] = function (a) function (b) a * b;
_['/'] = function (a) function (b) a / b;
var a = function () _['[]']();
var b = function () _[':'](1)(function () _['[]']());
var c = function () _[':'](1)(function () _[':'](2)(function () _['[]']()));
var d =
function ()
let (ff = function (i)
(i == 0)
? _['[]']()
: _[':'](1)(function () _['map'](_['+'](1))(ff(_['-'](i)(1)))))
ff(10);
var e = function () _[':'](1)(function () _['map'](_['+'](1))(e()));
var f = function () _[':'](1)(function () _['map'](_['+'](_['-'](3)(1)))(f()));
var g = function () _['map'](function (x) _['*'](x)(x))(f());
いかがだろうか。少しは見やすく……なったかもしれないということにしておこう。
ジェネレータを使った無限リスト
無限リストの値を利用するためにはいったんリストの計算を止めなくてはいけないが、完全に中止してしまうとそれ以降の値を求められない。途中の値を求め、かつ計算を続けるためには処理を中断する必要がある。中断、そう、今こそジェネレータの本領発揮である。JavaScript 1.8 からは、ジェネレータ式の導入により、
var g = (function () { for (var i in o) yield i + i; })();
を以下のように書けるようになり、よりジェネレータが扱いやすくなっている。
var g = (i + i for (i in o));
ここで、function キーワードがなくなっていることに注意してほしい。すなわち、ジェネレータ式を使えば、function キーワードを用いずに遅延評価を導入できるのである。
以下にジェネレータを使った無限リストの表現例を示す。[]、:、!!、+、-、*、/ は、便宜上それぞれ nil、cons、at、add、sub、mul、div と改名した。nil が生成するのは空のジェネレータイテレータであり、next メソッドが呼び出されると直ちに StopIteration 例外を投げて停止する。
var nil = function () { return; yield; };
var cons = function (x) {
return function (xs) {
yield x;
for (var i in xs)
yield i;
};
};
var map = function (f) {
return function (x) {
yield f(x.next());
for (var i in map(f)(x))
yield i;
};
};
var at = function (x)
function (i)
let (first = x.next())
(i == 0) ? first : at(x)(sub(i)(1));
var add = function (a) function (b) a + b;
var sub = function (a) function (b) a - b;
var mul = function (a) function (b) a * b;
var div = function (a) function (b) a / b;
var a = function () nil();
var b = function () cons(1)(nil());
var c = function () cons(1)(cons(2)(nil()));
var d = function ()
let (ff = function (i)
(i == 0)
? nil()
: cons(1)(map(add(1))(e for (e in ff(sub(i)(1))))))
ff(10);
var e = function () cons(1)(map(add(1))(i for (i in e())));
var f = function () cons(1)(map(add(sub(3)(1)))(i for (i in f())));
var g = function () (mul(x)(x) for (x in f()));
function take(iter, n) {
var i = 0;
for (var e in iter) {
if (++i > n) break;
yield e;
}
}
print([i for (i in a())]); // => (空文字列)
print([i for (i in b())]); // => 1
print([i for (i in c())]); // => 1,2
print([i for (i in d())]); // => 1,2,3,4,5,6,7,8,9,10
print(at(e())(2)); // => 3
print(at(f())(2)); // => 5
print([i for (i in take(e(), 5))]); // => 1,2,3,4,5
print([i for (i in take(f(), 5))]); // => 1,3,5,7,9
print([i for (i in take(g(), 5))]); // => 1,9,25,49,81
Kanasan.JS CodeReading #3 ― 2008年02月08日 22時17分
Kanasan.JS の Prototype.js CodeReading #3 (参加者のブログ記事一覧) に行ってきた。今回は告知が開催間際だったせいか人数はやや少なめだったけど、内容的にはこれまでと変わらぬ濃さ。範囲としては Prototype.js 1.6.0.2 の 1352 行目から 1650 行目付近まで。
コードリーディング
無線ネットワークが提供されているはずが私のマシンでは利用できず。LAN ケーブルをお借りして有線で接続。それにしても私がこれまでに参加した Kanasan.JS でネットワーク関係の不備に陥ること 4 回中 4 回。何か呪いでもかけられているのかと疑いたくなる。
例外処理の有無
Ajax.Response#getStatusText などは try 文による例外処理を行っているのに、Ajax.Response#getResponseHeader および Ajax.Response#getAllResponseHeaders に例外処理がないのはなぜかという話題 (例外処理の範囲はできるだけ小さくしたほうがいいだろうに)。例外が起きたときの処理を場合によって変えやすくするためではといった意見が。後から見直すと、XMLHttpRequest オブジェクトの同名のメソッドをそのまま呼び出すときだけ例外処理がないような気がする。XMLHttpRequest オブジェクトの処理を、例外も含めて透過的に扱えるようにするためか?
ちなみに、JavaScript では try 文が複数の catch 節を持てないという話が出たが、JavaScript 1.5 以降では可能だし、シンタックスは違うものの ECMAScript 4 でも可能になる予定だ。
// JavaScript 1.5 conditional catch clauses
try {
[].length = -1;
} catch (e if e instanceof TypeError) {
print("invalid type");
} catch (e if e instanceof RangeError) {
print("invalid index");
}
// => invalid index
// ECMAScript 4 multiple catch clauses
try {
[].length = -1;
} catch (e : TypeError) {
print("invalid type");
} catch (e : RangeError) {
print("invalid index");
}
// => invalid index
Ajax.Response#_getHeaderJSON
X-JSON ヘッダの値を変数 json に代入し、その後 json = decodeURIComponent(escape(json))
としている。これは、変数 json の値が UTF-8 バイト列だった場合、それを UTF-16 文字列に直す働きがある。まず escape 関数によって 0x80 - 0xFF の範囲の文字が %XX という文字列に置き換えられ、次に decodeURIComponent 関数によって %XX%XX%XX といった UTF-8 の URI エスケープ表現が元の文字に置き換えられる。
// 「日本語」のバイト表現は:
// UTF-8: E6 97 A5 E6 9C AC E8 AA 9E
// UTF-16BE: 65 E5 67 2C 8A 9E
var str = String.fromCharCode(0xE6, 0x97, 0xA5,
0xE6, 0x9C, 0xAC,
0xE8, 0xAA, 0x9E);
str.length; // => 9
str.charCodeAt(0).toString(16); // => e6
str = escape(str); // => %E6%97%A5%E6%9C%AC%E8%AA%9E
str = decodeURIComponent(str); // => 日本語
str.length; // => 3
str.charCodeAt(0).toString(16); // => 65e5
RFC 2616 によれば、RFC 2047 に従った形で符号化されない限り、HTTP ヘッダフィールドの値の文字符号化方式は ISO-8859-1 であるとみなされるようなので、UTF-8 オクテット列をそのまま送られたのなら、このような処理を挟む必要があるのだろう。実際のブラウザの実装がどうなっているかは知らないが。
Prototype.js では、プライベートなプロパティ、メソッドの名前はアンダースコア (_) から始まるようにしている。これは JavaScript の言語仕様によるものではないが、このようなコーディング規約を採用するところは多い (ぜんぜん見ないという声も)。
Ajax.Response#_getResponseJSON
HTTP ヘッダのフィールド名は大文字小文字を区別しない (RFC 2616 4.2)。XMLHttpRequest オブジェクトの getResponseHeader メソッドの引数として "Content-Type" を渡しても "Content-type" を渡しても、XMLHttpRequest オブジェクトが内部で同様に扱ってくれるはず。
if 文の条件節のインデントが不ぞろいで読みにくいという話から演算子の優先順位の話へ。論理和 (||) よりも論理積 (&&) のほうが優先順位が高い。この簡単な覚え方として、「和 (= 加算、+)」よりも「積 (= 乗算、*)」のほうが優先順位が高いのと同じというものがある。ビット和 (|) とビット積 (&) に関しても同様。最初、論理和演算子と論理積演算子の優先順位は同じと勘違いしていたのだが、そういう言語って何かあったっけ?
Node
Firefox や Opera、Safari 3 以降では、DOM 3 Core ECMAScript Language Binding に従って、Node コンストラクタ関数を提供している。Node コンストラクタ関数は Node インターフェースで定義された定数をプロパティとして持つ。
Node.ELEMENT_NODE; // => 1
Node.DOCUMENT_NODE; // => 3
document.nodeType == Node.DOCUMENT_NODE; // => true
Prototype.js では、IE でもこれらの定数が利用できるよう、Node コンストラクタ関数が存在しない場合は、新規オブジェクトを作成して Window オブジェクトの Node プロパティ (グローバル変数 Node) に設定している。
なお、Node コンストラクタ関数とはいっても、関数として呼び出したり、new 演算子とともにコンストラクタとして用いることはできない。ただし、Firefox や Opera、Safari 3 以降では、Node.prototype の値に変更を加えることで、Node インターフェースを実装するオブジェクトのプロパティ、メソッドを変更することができる。
Node.prototype.foo = 42;
Node.prototype.bar = function () { return this.nodeName; };
document.foo; // => 42
document.bar(); // => #document
// Document インターフェースは Node インターフェースを
// 継承しているので、document オブジェクトは
// Node インターフェースを実装しているといえる。
Element
Node と同じく、ブラウザによっては DOM Core の Element インターフェースに対して Element コンストラクタ関数 (ただし関数呼び出し、コンストラクタ呼び出しはできない) を提供している。しかし、Prototype.js では、Element をコンストラクタとして扱えるようにするため、Window オブジェクトの Element プロパティ (グローバル変数 Element) の値を書き換えている。その際、元の Element コンストラクタ関数のプロパティをそのまま扱えるようにするため、元の Element コンストラクタ関数を一時変数 element に退避している。
// Prototype.js 1.6.0.2 ll. 1546-1561
(function() {
// ここで元の Element コンストラクタ関数の値を退避。
var element = this.Element;
// Element プロパティの値に関数を設定することで、
// Element コンストラクタとして使えるようにする。
this.Element = function(tagName, attributes) {
...
// ここで、Element.cache の Element は
// Element プロパティに新しく設定された無名関数。
// グローバル変数 Element とはグローバルオブジェクト
// (ブラウザ上では Window オブジェクト) の
// Element プロパティのことであり、この関数内の
// コードが実行される時点で Element プロパティの
// 値は既に無名関数に書き換えられているため。
var cache = Element.cache;
...
};
// 元の Element コンストラクタ関数のプロパティを、
// 新たな Element コンストラクタ上でも使えるようにする。
Object.extend(this.Element, element || { });
}).call(window);
全体を関数式で囲んでいるのは一時変数 element をグローバルに公開しないためか。単に (function() { ... })()
とするのと変わらないはずなのに、なぜわざわざ (function() { ... }).call(window)
と call メソッドを使っているのかは不明。
IE では要素の name 属性の値を動的に変更することができないので、IE で name 属性を持つ要素を作成するときは場合分けをして、document.createElement メソッドの引数に、name 属性を含めた開始タグを渡すようにしている。createElement メソッドの引数として要素名だけでなく開始タグも受け付けるというのは、もちろん IE の独自拡張仕様。
要素を作成する際、キャッシュを用意して cloneNode メソッドを呼び出すことで新しく要素のコピーを作成している。多くのブラウザでは、毎回 createElement メソッドを使って新しく要素を作るよりもパフォーマンスがよいようだ。
IE 6.0 SP2 | Opera 9.25 | Firefox 2.0.0.12 | Firefox 3.0b4pre | |
---|---|---|---|---|
createElement | 1125 | 1344 | 3140 | 1701 |
createElement (document を変数に保持) | 640 | 969 | 3062 | 1522 |
cloneNode | 500 | 750 | 2343 | 1194 |
Element.Methods
まだ読み進めてないが、Element.Methods で定義されたメソッド x は、Element.x(element, args)
と対象の要素を引数にとって呼び出すことも、element.x(args)
と対象の要素のメソッドとして呼び出すこともできるようになるのだろう。
Element.Methods.update
Object.toHTML メソッドは、引数が toHTML メソッドを持っていればその返り値を、そうでなければ引数を文字列化した値を返す。ただし、引数が null または undefined のときは空文字列を返す。よって、element.update()
とすると element.innerHTML に空文字列が設定され、結果として element の内容がすべて削除されることになる。
innerHTML に script 要素を含んだ文字列を設定してもそのスクリプトは実行されない。そこで、文字列から script 要素の内容を抜き出してスクリプトとして評価している。この処理には defer メソッドが適用され、update メソッドの終了後にスクリプトの評価が行われるので、処理時間の長いスクリプトが含まれていても update メソッドの実行が長引くことはない。
Element.Methods.replace
update メソッドが対象の要素の内容を置き換えるのに対し、replace メソッドは対象の要素そのものを置き換える。
DOM 2 Range (IE 7 は非対応) および Mozilla 独自拡張の createContextualFragment メソッド (SafariおよびOpera 9以降でも対応) を使っているのに、なぜ IE でも動くのかと思ったら、2445 行目以降で要素オブジェクトが outerHTML プロパティを持つ場合 (IE、Opera、Safari が該当) はメソッドの内容を書き換えていた。
Element.Methods.insert
引数に { before: beforeContent, bottom: bottomContent }
といったオブジェクトを渡すことで、複数の箇所に内容を挿入できる。そうでなければ bottom の位置 (要素の元の内容の終端) に挿入される。
Element._getContentFromAnonymousElement
Element.insert の内部などで使われているヘルパメソッド。HTML のソースコードを文字列として受け取り、それを DOM ノードに変換して配列として返す。引数は tagName、html の 2 つでいずれも文字列型。html は変換する HTML のソースコードを表し、tagName は html を内容として持つ要素名 (親要素名) をあらわす。たとえば、p 要素の内容の終端に <em>Hello</em>, world!
という内容を挿入したいとき、
// 変数 p はある p 要素を指す。
Element.insert(p, "<em>Hello</em>, world!");
// p.insert({ bottom: "<em>Hello</em>, world!" }) としても同じ。
と insert メソッドを呼び出したとすると、その内部で、
Element._getContentFromAnonymousElement("P", "<em>Hello</em>, world!")
と _getContentFromAnonymousElement が呼び出される。_getContentFromAnonymousElement の内部では、新たに div 要素を作成し、その innerHTML プロパティに引数 html の値 (ここでは "<em>Hello</em>, world!") を設定した後、その childNodes プロパティを配列に変換して返す。よって、この場合は、em 要素ノードとテキストノード (表す文字列は ", world!") との 2 要素を収めた配列が返ってくる。
しかし、このままでは、たとえば表の行にセルを追加したいときなどに対応できない。このとき、
Element._getContentFromAnonymousElement("TR", "<th>Header</th><td>Cell</td>")
と呼び出されるが、div 要素の直接の子要素として th、td 要素をとることはできないからだ。そこで親要素名 "TR" が用いられることになる。特定の親要素名に対しては、前後にタグを補完した値が div 要素の innerHTML プロパティに設定される。この場合は、
div.innerHTML = "<table><tbody><tr>" +
"<th>header</th><td>Cell</td>" +
"</tr></tbody></table>";
といった具合だ。その後、tr 要素の childNodes プロパティを配列に変換して返すことになる。つまり、ここでの返り値は th 要素ノードと td 要素ノードを収めた配列だ。このとき、ローカル変数 div は、最終的に div 要素ではなく tr 要素を指すようになるので注意が必要。
懇親会、その他
IE では table 要素の直下に tr 要素を追加しても表示されないとか、HTML 4 では tbody 要素の開始タグと終了タグは省略可能だけど tbody 要素自体は table 要素の子どもに必ず存在するとか、PHP 4 と VB 6 はしぶとく生き残り続けるんじゃないのとか、PHP 4 == Perl 4 とかいった話題が出ていたような気がする。getElementsByTagName メソッドの引数に "*" を指定して全要素が取れるのはIE 5.5以降と言ったけど、帰ってから試したら IE 6 以降みたいだ。
そんなこんなでコードリーディング以外にもいろいろと興味深い話が聞け、得るものの多い一日だった。Kanasan さんはじめ参加者の皆さんお疲れ様 & ありがとう。
JavaScript でカリー化、再び ― 2008年02月14日 05時04分
以前、「JavaScript で引数束縛」において関数のカリー化を試みました。しかし、そこでカリー化された関数は、そのままでは一度しか部分適用ができず、また、最初の関数呼び出しは必ず部分適用として扱われていました。
function mean3(a, b, c) { return (a + b + c) / 3; }
// 「JavaScript で引数束縛」における curry 関数。
var curriedMean3 = curry(mean3);
curriedMean3(1)(2, 3); // => 2
curriedMean3(1)(2)(3);
// => TypeError: curriedMean3(1)(2) is not a function
// そのままでは部分適用を 2 回以上行えない。
// curry(curriedMean3(1))(2)(3) なら大丈夫。
curriedMean3(1, 2, 3);
// => function () { ... }
// 十分な数の引数が与えられているにもかかわらず、
// 部分適用とみなされて関数が返る。
そこで、この制限を取り除いた――すなわち、1 回カリー化するだけで複数回の部分適用が行え、十分な数の引数が与えられれば直ちに結果を返すような関数を返す――カリー化関数を作ってみようと思います。
このカリー化関数の名前を curry としましょう。curry 関数は、引数として関数を受け取り、その関数をカリー化した関数を返します。とりあえず、関数を受け取り関数を返すというのをコードにしてみます。
function curry(f) {
return function () {
...
};
}
返される関数の中では何を行えばいいのでしょうか。それはこの関数が呼び出されたときの引数の数によって異なってきます。十分な数の引数が与えられたなら、元の関数を呼び出し、その結果を返してやらなくてはいけません。ある関数 (Function オブジェクト) が必要とする引数の数は、その Function オブジェクトの length プロパティから得ることができるので、これを利用してやります。
function curry(f) {
return function () {
if (arguments.length >= f.length)
return f.apply(null, arguments);
...
};
}
これで「十分な数の引数が与えられたら直ちに結果を返す」ことは達成できました。引数の数が足りなかった場合は、部分適用がなされたものとみなして、関数を返すことにします。
function curry(f) {
return function () {
if (arguments.length >= f.length)
return f.apply(null, arguments);
return function () {
...
};
};
}
部分適用の結果として返ってくる関数とは何でしょうか。それは、足りない引数を受け取り、元々の関数を実行する関数です。これを実現するためには、部分適用された引数の値を覚えておく必要があります。
function curry(f) {
return function () {
if (arguments.length >= f.length)
return f.apply(null, arguments);
var args = Array.prototype.slice.call(arguments);
return function () {
return f.apply(null, args.concat(Array.prototype.slice.call(arguments)));
};
};
}
Array.prototype.slice.call(arguments)
というのは Arguments オブジェクトを配列に変換するための決まり文句のようなものです。Array オブジェクトの slice メソッドは、配列の一部を抜き出し新たな配列として返しますが、引数を省略すると 0 番目から length - 1 番目までの要素を抜き出したもの、すなわち元の配列全体のコピーを返します。これを Arguments オブジェクトに適用することで、Arguments オブジェクト全体をコピーした配列が返ってくるというわけです。JavaScript 1.6 以降なら、Array generics により Array.slice(arguments)
と書くこともできます。
しかし、このままでは相変わらず 1 回のカリー化につき 1 回の部分適用しかできません。複数回の部分適用ができるようにするためには、部分適用の結果として返ってくる関数も、部分適用ができるようにする必要があります。部分適用ができるようにするためにはどうすればいいか、そう、カリー化です。とりあえず 1 回の部分適用ができるようになるカリー化関数はできているのですから、それを再度適用してやればよいのです。
function curry(f) {
return function () {
if (arguments.length >= f.length)
return f.apply(null, arguments);
var args = Array.prototype.slice.call(arguments);
return curry(function () {
return f.apply(null, args.concat(Array.prototype.slice.call(arguments)));
});
};
}
これで大丈夫かと思いきや、まだ問題があります。カリー化を行うためには、関数が必要とする引数の数がわかっていなくてはいけません。部分適用の結果として返される関数が必要とする引数の数は、元々の関数の引数の数から部分適用された引数の数を引いたもの (curry 関数が返す関数の中では f.length - arguments.length
) になるはずです。しかし、ここでは仮引数が 0 個の関数を使っているため、部分適用の結果として返される関数に対するカリー化がうまくいきません。
関数式や関数宣言を使う以上、仮引数の数を動的に変えることはできません。Function オブジェクトの length プロパティが書き込み可能ならば問題はないのですが、あいにくこのプロパティは読み込み専用です。そこで、仮引数の数を動的に変えて関数を作るために、Function コンストラクタを利用することにします。
function setParameterLength(f, n) {
var funcs = arguments.callee.funcs;
if (!(n in funcs)) {
var argNames = [];
for (var i = 0; i < n; i++)
argNames.push("_" + i);
funcs[n] = new Function("f",
"return function (" + argNames.join(", ") +
") { return f.apply(this, arguments); };");
}
return funcs[n](f);
}
setParameterLength.funcs = [];
setParameterLength 関数は、引数として関数 f と数 n を受け取り、関数を返します。返される関数の仮引数の数は n (length プロパティの値は n) であり、実行すると f がそのまま呼び出されます。これを使うことで、部分適用の結果として返る関数にも正常にカリー化を適用できるようになります。
function curry(f) {
return setParameterLength(function () {
var restLength = f.length - arguments.length;
if (restLength <= 0)
return f.apply(null, arguments);
var args = Array.prototype.slice.call(arguments);
return curry(setParameterLength(function () {
return f.apply(null, args.concat(Array.prototype.slice.call(arguments)));
}, restLength));
}, f.length);
}
これで完成です。それでは実際に試してみましょう。
var curriedMean3 = curry(mean3);
curriedMean3(1, 2, 3); // => 2 (引数がそろえばそのまま呼び出し)
curriedMean3(1, 2)(3); // => 2 (部分適用が可能)
curriedMean3(1)(2)(3); // => 2 (複数回の部分適用も可能)
curriedMean3()(1)()(2)()(3); // => 2
curriedMean3(1, 2)(3, 4); // => 2 (mean3 は第 4 引数を使わない)
うまくいきましたね。余分な引数を与えた場合、元々の関数にもその引数は渡されますが、この場合それは使われません。
さて、カリー化ができたのはいいですが、このようなことはきっと誰かがやっているだろうなと検索してみたところ、svendtofte.com - Curried JavaScript functions というページが見つかりました。そこに載っている考え方に沿ってカリー化関数を書くと以下のようになります。
function curry(f) {
if (f.length == 0) return f;
function iterate(args) {
if (args.length >= f.length)
return f.apply(null, args);
return function () {
return iterate(args.concat(Array.prototype.slice.call(arguments)));
};
}
return iterate([]);
}
いきなり関数を返すのではなく、元々の関数を呼び出すか部分適用を行うかの処理を内部関数に切り出すことで、元々の関数を呼び出す部分が一箇所にまとまっています。内部で curry 関数を再度呼び出しているわけではないので、仮引数の数を調整する必要もありません。無引数の関数は部分適用をする意味がないので、その場合は渡された関数をそのまま返しています。
Kanasan.JS JavaScript 第 5 版読書会 #3 ― 2008年02月27日 04時47分
Kanasan.JS の JavaScript 第 5 版読書会 #3 (当日のチャットログ、参加者のブログ記事一覧) に行ってきました。同じ会場で先立って vim 勉強会があったのですが、私は先日 Meadow に乗り換えたばかりなのでそちらのほうはパスしました。
配列のプロパティ
JavaScript ではすべてのプロパティ名が文字列として扱われます。配列 a
に対して a[-1.23] = true
としても、a["-1.23"] = true
と同じに扱われ、"-1.23" という名前のプロパティができるだけでエラーにはなりません。ただし、配列に関しては非負整数とみなされる名前のプロパティが特別扱い (length プロパティにも影響を及ぼしうる) されます。
Array#join と Array#toString
配列に対して、join メソッドを引数なしで呼び出すのと toString メソッドを呼び出すのとでは同じ結果が返ってきます。ただし、join メソッドは配列のようなオブジェクト (配列ではないが length プロパティを持つオブジェクト) に対しても適用できるのに対し、配列の toString メソッドは配列にしか適用できないという違いがあります。
var arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
Array.prototype.join.call(arrayLike);
// => a,b,c
Array.prototype.toString.call(arrayLike);
// => TypeError: Array.prototype.toString called on incompatible Object
そのため、実装によっては toString メソッドのほうが高速化できるかもしれません。ちなみに Firefox などで使われている SpiderMonkey の場合は、join メソッドも toString メソッドも内部的に同じ関数 (array_join_sub 関数) を利用しています。
Array#push、Array#unshift の返り値
push メソッド、unshift メソッドが要素追加後の配列の要素数を返すのはどうしてかという話題。これらのメソッドは Perl 由来なので返り値も Perl に従ったのでしょうが、なぜ Perl がそうなっているのは不明です。個人的には配列自身を返してくれたほうが何かと使いやすいと思うのですが。
Array#slice と Array#splice
slice メソッドと splice メソッドの字面が似ていて間違えやすいという声がありました。Perl や Ruby の場合は slice に相当する構文が別途存在する (@array[1..3]
) ので splice があっても混乱しないんだとか。ECMAScript 4 でもスライス構文 (array[1:3]
) が定義される予定です。
ちなみに ECMAScript では splice の第 2 引数は必須ですが、これを省略すると IE と Firefox とで動作が違ってくるそうです。
配列の各要素の比較
配列の各要素が等しいかを調べようと思って array1 == array2
と等値演算子を使ってもうまくいきません。配列はオブジェクトであり、等値演算子をオブジェクト同士の比較に用いると両者が同一のオブジェクトかどうかを返すからです。
解決策として、配列の要素がすべて文字列だとわかっていれば、join メソッドの結果を比べるといった方法があります。区切り文字を NUL 文字 ("\x00"
) にすれば文字がかぶることも減るでしょう。または、Prototype.js を使っていれば、 toJSON メソッドを使って JSON 形式に変換した文字列を比べるという方法もあるそうです。
関数宣言の構文
関数宣言の最後にセミコロンはいりません。これは入れ子になった関数宣言に対しても同じです。JavaScript にはセミコロンの自動補完があるので、本来セミコロンがいるのかどうか迷ってしまいますね。
function f() {
function g() {
...
}; // ← このセミコロンは不要 (この場合空文と解釈される)。
...
} // ← ここもセミコロンは不要。
関数式、関数リテラル、無名関数
個人的には、関数式と関数リテラルは構文の名前 (同一のものを関数式と呼ぶこともあれば関数リテラルと呼ぶこともある) で、無名関数とは識別子を指定していない関数式 (関数リテラル)、または Function コンストラクタによって作られる関数オブジェクトのことであると理解しています。(私も場合によっては意図的に混同させて使いますが。)
関数式 (関数リテラル) を使って名前つきの関数オブジェクトを作ることもできますが、JScript では関数式に指定した名前が外部に公開されたり、古い JavaScriptCore (Safari 2 以前) ではそもそも名前付き関数式に対応していなかったりするので、Web 上での使用は避けたほうがいいかもしれません。
var f = function g() { ... }; // 名前付き関数式
g(); // JScript ではエラーにならない。
負の無限大
Number.NEGATIVE_INFINITY
、-Infinity
、-1 / 0
はすべて同じ値 (負の無限大) を指します。
関数のオーバーロード
JavaScript では関数のオーバーロードはできないので、同様の機能を実現しようとしたら自分で引数をチェックとして処理を振り分ける必要があります。オーバーロードを実現するためのライブラリも多数公開されているようです (Introducing overload.js、JavaScript Method Overloading など)。
PascalCase
変数などの命名規則について。UpperCamelCase のことを PascalCase ともいうそうです。JavaScript 組み込みのメソッド名は基本的に lowerCamelCase なので、私はそれに合わせているのですが、アンダースコア区切りのほうがスコープが狭い気がするのでローカル変数はアンダースコア区切りという人もいました。
Function#length
関数オブジェクトの length プロパティ (仮引数の数を表す) は読み取り専用なので値を上書きできません。ただし、JavaScript では読み取り専用のプロパティに値を設定しようとしてもエラーは出ません。SpiderMonkey の strict モードでは警告が出ます。
function f(a, b, c) {}
f.length = 42;
// => strict warning: f.length is read-only
// => 42
// 代入式の返り値は右辺の値。
f.length;
// => 3
読み取り専用のプロパティを新しく作ることはできません。SpiderMonkey 1.7 (Firefox 2) までは Object.prototype.eval と const 文を組み合わせることにより実現可能でしたが、SpiderMonkey 1.8 (Firefox 3) で Object.prototype.eval は削除されます。
var o = {};
o.eval("const x = 42;");
o.x = 12;
o.x; // => 42 (o.x は読み取り専用になる)
カリー化と部分適用
個人的な理解では、一部の引数の値を固定するのが部分適用 (n 引数関数と m 個の値を受け取り、n - m 引数関数を返す)、ある関数を部分適用可能な関数に変換する操作がカリー化です。
最近のコメント