Linq to ObjectsをJavaScriptに実装する方法
- 2010-04-11
JavaScriptでLINQを使おう - 複雑な検索処理を簡潔に記述する「JSINQ」という記事が出ました。私はlinq.jsという、同種のJavaScriptへのLINQ移植ライブラリを作成している人間のため、JSINQの人気っぷりに思わず嫉妬してしまった(笑)のですが、そういう感情は抜いておいてこの紹介記事は、今ひとつよろしくない。
まず頂けないのが、列挙の方法。
// 生のenumeratorを取り出して列挙するですって!?
while (enumerator.moveNext()) {
var name = enumerator.current();
document.write(name + '<br>');
}
// eachが用意されているというのに!
result.each(function(name) { document.write(name + "<br />") });
C#でもJavaでも、きっと他の言語でも、反復子をwhileループで回すなんて原始的なことは普通やりませんよね? foreachに渡しますよね? そんなわけでJSINQにはeachメソッドが用意されているのですが紹介記事は普通にスルー。「enumeratorでの列挙とeachでの列挙二つ紹介する」「enumeratorのみ紹介する」「eachのみ紹介する」の三択で、スペースの都合上一つしか紹介出来ないなら、eachのほうを紹介すべきでしょう。いやまあ、例が一個だったらしょうがないなあ、どうせJSINQのチュートリアルの上のほうから抜き取っただけだろうしー、と思うのですが、三個もenumerator取り出しの例を出されるとさすがにオイオイオイオイ、と突っ込みたくなる。
もうひとつは、文字列によるクエリ構文を推しすぎ。JSINQの最大の特徴でもある部分なのでJSINQの紹介としては正しいのですが(JSINQのプロジェクトページでもそれをフィーチャーしてますしね)、LINQの紹介として見ると大変頂けない。.NETを知らない人(JavaScriptのライブラリなので、基本はJavaScriptの人が見るでしょう)がLinqを誤解してしまう要因になりうるので、こういった紹介は割とキツい。
LINQとはLanguage Integrated Query(統合言語クエリ)であり、言語に統合されていてこそLinqなのです。文字列で与えたらSQLと一緒。LinqはしばしばSQLっぽく記述するもの、と誤認されているようですが、違います。文字列で与えていたSQL(こんな風にね、と最近作ったDbExecutorというSQL実行簡易補助ライブラリをどさくさに紛れて紹介してみる)とは全く別物なのです。詳細は説明すると長くなるので省いちゃいます(え?)。理屈はともかく、言語に統合されていない状態でのSQLは書きやすいとはいえないわけですよ?
var elements = document.getElementsByTagName('a');
var enumerable = new jsinq.Enumerable(elements);
var query = new jsinq.Query(' \
from e in $0 \
where e.href.indexOf("google.co.jp") > -1 \
select e \
');
query.setValue(0, enumerable);
var result = query.execute();
var enumerator = result.getEnumerator();
while (enumerator.moveNext()) {
var e = enumerator.current();
document.write(e.text + ': ' + e.href + '<br>');
}
改行のために末尾に\を入れなければならない、不恰好なプレースホルダ、クエリコンパイルの必要性(executeメソッドの実行でメソッドチェーン形式に変換されます、面白いことにこの点まで.NET Frameworkの忠実な再現となっています(クエリ構文はメソッド構文の糖衣構文にすぎない))。というわけで、到底書きやすいとは言えません。この例を見て、長げーよ馬鹿、意味ねー、アホじゃねーの?普通にfor回した方が百億倍マシだろ、と思った人もいるでしょう。その通りです。素直に便利かも……とか思ったなら、物事はもう少し冷静に見るようにしてください。しかしメソッド構文(jQueryのようにメソッドチェーンで書く方法)ならこう書けます。
var elements = document.getElementsByTagName('a');
new jsinq.Enumerable(elements)
.where(function(e) { return e.href.indexOf("google.co.jp") > -1 })
.each(function(e) { document.write(e.text + ": " + e.href + "<br>") });
これなら納得で、割と使えるかもって感じではないでしょうか? JSINQにおける文字列によるクエリ構文は、人を釣るためのただの餌です。そんな餌で俺様が釣られクマー。jSINQをJavaScriptライブラリとして使うのならば、メソッド構文のほうをお薦めします。クエリ構文はネタ、もしくはただの技術誇示にすぎません。よくやるなー、って感じで素晴らしいとは思いますが、実用性は皆無です。JSINQ自体はLinqの移植として割と良く出来ているので(何だこの上から目線)、文字列クエリ構文で試してみて使えないなー、と思ってしまった、もしくは紹介を見て文字列クエリ構文とかこのライブラリダメだろ、と思った人は、その辺は誤解なくどうぞ。
linq.js
LinqのJavaScript実装は他にもあります。一つは、ええと、私の作成しているlinq.jsです。売り文句はJSINQと同じくSystem.Enumerableとの完全なるAPI互換。.NET4までの範囲を全てカバーしています。更にその上に、Achiral, Ruby, Haskellなどから参考にした大量のメソッドが追加されていることと、Visual Studioで使う場合にはIntelliSenseが動作するファイルがあること、などなど「実用的に使う」ことを強く意識して作っています。手前味噌なのでアレですが、他のどのライブラリよりも使える度は高いと思っています。
- linq.js - LINQ for JavaScript Library - プロジェクトページ
- 紹介と簡単なチュートリアル
- 入力補完に対応させたのでVSでの利用法紹介
- ブログ記事のlinq.jsカテゴリ(最後の更新が去年の9月、がーん)
更新が微妙に止まっているのですが、WindowsScriptHostで快適に使えるような追加ライブラリを作成中(と、9月に言ったっきり絶賛作業休止中、すみません、でもやる気はあるので遠くないうちに必ず出します)。あと、Reactive Extensions for JavaScriptという、これまた.NET発のJavaScript移植ライブラリが出ているので、それとの協調動作も考えています。
Linqを自分で実装する
では本題。実際にLinqをJavaScriptで実装してみましょう。C#でSelectを実装してみたことはありますか? 何のことはなく、たった1行で出来ちゃうんですよね。そんなわけで、実際のところ別に難しいことはありません。勿論、全てのAPIを網羅するのは面倒くさいですが、基本的な原理を掴んでおくとグッと利用法が広がるはずです。まずは、一番単純な、Array.prototypeに生やす方法を考えてみます。例としてmapとforEachを実装してみましょう。
Array.prototype.map = function(selector) {
var result = [];
for (var i = 0; i < this.length; i++)
result.push(selector(this[i]));
return result;
}
Array.prototype.forEach = function(action) {
for (var i = 0; i < this.length; i++)
action(this[i], i); // with index
}
var array = [1, 2, 3, 4, 5, 6, 7, 8, 9];
array.map(function(i) { return { Single: i, Double: i * 2} })
.forEach(function(a) { alert(a.Single + ":" + a.Double) });
配列を変形してforeach。非常に単純な代物ですが、単純が故にmapやfilterは便利ですよね、かなり多用します。C#における匿名型は、JavaScriptではそのままハッシュを返すことで実現されます。さて、このやり方には問題が二つあります。一つはビルトインオブジェクトのprototypeを拡張する、微妙なお行儀の悪さ。そこで、arrayを独自オブジェクトにくるんでやりましょう。
function Enumerable(array) {
this.source = array;
}
Enumerable.prototype.map = function(selector) {
var result = [];
for (var i = 0; i < this.source.length; i++)
result.push(selector(this.source[i]));
return new Enumerable(result);
}
Enumerable.prototype.filter = function(predicate) {
var result = [];
for (var i = 0; i < this.source.length; i++)
if (predicate(this.source[i])) result.push(this.source[i]);
return new Enumerable(result);
}
Enumerable.prototype.reduce = function(func) {
var result = this.source[0];
for (var i = 1; i < this.source.length; i++)
result = func(result, this.source[i]);
return result;
}
var array = [1, 2, 3, 4, 5, 6];
var sum = new Enumerable(array)
.filter(function(i) { return i % 2 == 0 })
.map(function(i) { return i * i })
.reduce(function(x, y) { return x + y });
alert(sum); // 56
配列を一旦包まなくてはならないのが煩わしいのですが、メソッドチェーンのコンボを決めて、気持ちよく列挙することが出来ます。この例ではFirefoxのfilter, map, reduceを再定義してみました(thisObjectの辺りはスルーしてますしreduceの引数なんかも違いますが)。1から6の配列のうち偶数のみを二乗して足し合わせる。答えは56。さて、しかしこの方式にも問題があります。Arrayのprototype拡張が抱えているもう一つの問題と同じですが、メソッドの一つ一つを通る度に無駄な中間配列を生成してしまっています。メソッドチェーンの形になっていると隠蔽されてしまうのですが、冷静に眺めてみればこういうことです。
var array = [1, 2, 3, 4, 5, 6];
var _array = [];
for (var i = 0; i < array.length; i++) {
if (array[i] % 2 == 0) _array.push(array[i]);
}
var __array = [];
for (var i = 0; i < _array.length; i++) {
__array.push(_array[i] * _array[i]);
}
var sum = __array[0];
for (var i = 1; i < __array.length; i++) {
sum += __array[i];
}
さすがに、これはあまりのアホさと無駄さに死ね!と言いたくなりませんか?まあ、この程度は大したコストではないのも確かですし、これこそが富豪的プログラミングだ!といえば、そうだし、その辺はそんなに否定しません。些細なパフォーマンスチューニングにはあまり興味ありません。が、しかし、根本的な問題として、これだと無限リストが扱えません。無限リストとは無限に続くもの、例えば [0,1,2,...,9999,10000,...] 。そんなの使わないって?いやいや、使いこなすと存外便利ですよ? そんなわけで、富豪とか云々を抜きにしても、ただのArrayラッパーは却下です。即時評価なfilterやmapなんて使いたくありません。.NET FrameworkのLinq to Objectsは遅延評価なので、無限リストも扱えますし中間配列といった無駄は出てきません。では遅延評価のリスト処理をどう実装しましょうか。無限リストを作る方法は色々あるでしょうが、ここはLinqの移植なのでC#でのやり方と同じくイテレータパターンを用います。
IEnumerable = function(moveNext) {
this.getEnumerator = function() {
return { current: null, moveNext: moveNext }
}
}
// Generator
Enumerable =
{
toInfinity: function(from) {
if (from === undefined) from = 0;
return new IEnumerable(function() {
this.current = from++;
return true;
});
}
}
// select as map
IEnumerable.prototype.select = function(selector) {
var source = this;
var enumerator = null;
return new IEnumerable(function() {
if (enumerator == null) enumerator = source.getEnumerator();
if (enumerator.moveNext()) {
this.current = selector(enumerator.current);
return true;
}
return false;
});
}
// 無限に2倍するリスト[0, 1, 4, 9, 16,...
Enumerable.toInfinity().select(function(i) { return i * 2 });
LinqはIEnumerableオブジェクトの連鎖で成り立っています。また、return thisでメソッドチェーンをするわけではありません。selectを見てください。メソッドが呼ばれた時点では何も実行せずに、クロージャにより環境を保持した新しいIEnumerableを生成し、それを返しています。ではいつ実行されるのかというと、getEnumerator()が呼ばれ、それで取得されたenumeratorオブジェクトのmoveNext()を呼んだ時です。
さて、しかしこのままではgetEnumeratr()で反復子を取得しての列挙しか出来なくて不便なので、forEachなどを定義してやる必要があります。また、無限リストが本当に無限のままでは困るので、停止させるものが必要です。というわけで、代表的なものを幾つか紹介します。
IEnumerable = function(moveNext) {
this.getEnumerator = function() {
return { current: null, moveNext: moveNext }
}
}
// Generator
Enumerable =
{
from: function(array) {
return Enumerable.repeat(array)
.take(array.length)
.select(function(ar, i) { return ar[i] });
},
toInfinity: function(from) {
if (from === undefined) from = 0;
return new IEnumerable(function() {
this.current = from++;
return true;
});
},
repeat: function(element) {
return new IEnumerable(function() {
this.current = element;
return true;
});
}
}
// select as map
IEnumerable.prototype.select = function(selector) {
var source = this;
var enumerator = null;
var index = -1;
return new IEnumerable(function() {
if (enumerator == null) enumerator = source.getEnumerator();
if (enumerator.moveNext()) {
this.current = selector(enumerator.current, ++index);
return true;
}
return false;
});
}
// where as filter
IEnumerable.prototype.where = function(predicate) {
var source = this;
var enumerator = null;
var index = -1;
return new IEnumerable(function() {
if (enumerator == null) enumerator = source.getEnumerator();
while (enumerator.moveNext()) {
if (predicate(enumerator.current, ++index)) {
this.current = enumerator.current;
return true;
}
}
return false;
});
}
IEnumerable.prototype.take = function(count) {
var source = this;
var enumerator = null;
var index = -1;
return new IEnumerable(function() {
if (enumerator == null) enumerator = source.getEnumerator();
while (++index < count && enumerator.moveNext()) {
this.current = enumerator.current;
return true;
}
return false;
});
}
IEnumerable.prototype.toArray = function() {
var result = [];
var enumerator = this.getEnumerator();
while (enumerator.moveNext()) {
result.push(enumerator.current);
}
return result;
}
// 利用例
// こんな配列があったとして
var array = [1232, 421, 1, 2, 3412, 42, 4, 2, 45];
// 偶数のもののみ二倍した新しい配列を生成
var array2 = Enumerable.from(array)
.where(function(i) { return i % 2 == 0 })
.select(function(i) { return i * 2 })
.toArray();
// 1-100の配列を作成
var array3 = Enumerable.toInfinity(1).take(100).toArray();
// ""のみの長さ100の配列を作成
var array4 = Enumerable.repeat("").take(100).toArray();
生成用メソッドとして、配列を反復子に変換するfrom, 無限にインクリメントした整数を返すtoInfinity, 無限に同一要素を繰り返すrepeatを定義しました。メソッドチェーン用として関数を要素に適用させるselect, 関数でフィルタリングするwhere, 指定個数取得するtake。そしてメソッドチェーンを打ちきって通常使えるオブジェクトに変換するものとして、配列に変換するtoArrayを定義。
fromがrepeatとtakeとselectの組み合わせで出来ているというのが、面白いところです。所謂Fill(配列の初期化)も、repeat->take->toArrayで出来てしまいます。小さなパーツを組み合わせてあらゆることを出来るようにするのがLinqの魅力です。
速度?これが速いと思いますか?そうですねえ、見るからに、xxxですね。しかし、私はミリセカンド単位でのパフォーマンスチューニングにはあまり興味がありません。はいはい、富豪的富豪的。実際のとこGoogle Chrome使えばIE6の1000倍速くなるんだぜ!(数値は適当)。って感じなので、JavaScript側での最適化は、あまり……。とくにLinqではDOM操作とか重たいことをやるんではなくて、純粋に、連鎖の分だけ関数呼び出しが増えるって程度でしかないので、この程度のことでムダムダムダムダー、と言ってもしょうがない気がします。なので、そんなことは気にしないことにします。
ラムダ式もどき
function(x,y,...){return ...}は、長い。Firefoxならfunction() ... で書けるけれど、それでも長い。というわけで、linq.jsでは文字列でラムダ式風に記述出来るようにしています。
var CreateLambda = function(expression) {
if (expression.indexOf("=>") == -1) {
return new Function("$", "return " + expression);
}
else {
var expr = expression.match(/^[(\s]*([^()]*?)[)\s]*=>(.*)/);
return new Function(expr[1], "return " + expr[2]);
}
}
var lambda = CreateLambda("i=>i*i");
var r = lambda(3); // 9
E.Range(1,10).Where("$%2==0").Select("$*$") // linq.jsではこんな感じで書ける
「引数=>式」で文字列を与えます。引数が一つ以下の場合は=>を省略出来ると同時に、$が引数の値として使えるようになっています(Scalaの_とかこんな感じ、なはず)。実装は見た通り非常に単純で文字列分解してnew Functionに渡して関数作ってるだけ。これの難点は、クロージャにならないので、変数のキャプチャが出来ないことです。まあ、そういう時は諦めて無名関数作ってください。
まとめ
filterやmapやreduceが使えて、distinct(重複除去、いわゆるuniq)が使えて、遅延評価だったり、selectやwhereを何段もポコポコと追加出来るわけです。linq.jsはシンプルなライブラリです。派手な機能は一切ありません。ただ列挙して処理するメソッドしかありません。DOMなど一切触りません(DOMの列挙自体は可能なので、DOMノードを流してフィルタリングしたり加工したり、というのは有益でしょう)。ただ、それ故に、使い道は無限大です。
微妙に更新止まってます、が、やる気はあります!まずはWSH対応から!と言いたいのですが、現在は何故かJava移植の制作を進めています。Javaにも素晴らしいLinq to Objectsの世界を、忠実移植で。というわけなのですが、これも先月ぐらいからやるやる詐欺中。中身は完全に出来上がっていて現在テストとJavaDoc書き中。今月中にはリリースしたい、ですね。先月も同じこと言ってましたが、まあ、着々と鈍足ながらも進んでいるので、近いうちにはお見せできるはずです。
ともあれ、Linq to Objectsは大変素晴らしいので、C#な人はガンガン使って欲しいし、JavaScriptの人はlinq.jsを試して欲しいし、Javaな人はもう少し待ってください。私は、ええと、このサイトのC#カテゴリのほとんどがLinq絡みです、ひたすらに使い倒して、有用な使い方を紹介していけたらと思っています。