linq.js - JavaScript用LINQライブラリ

概要

JavaScriptでC#のLinq to Objectsを実現するライブラリ。

// C# LINQ
Enumerable.Range(1, 10).Where(i => i % 3 == 0).Select(i => i * 10);
// JavaScript + linq.js 1
E.Range(1, 10)
  .Where(function(i) { return i % 3 == 0; })
  .Select(function(i) { return i * 10; });
// JavaScript + linq.js 2
E.Range(1, 10).Where("i=>i%3==0").Select("i=>i*10");
// JavaScript + linq.js 3
E.Range(1, 10).Where("$%3==0").Select("$*10");

こんな風に記述できます。この例は1から10の整数を発生させ、そのうち3で割り切れる数を10倍にするというもので、結果は30,60,90になります。Linq to Objectsは、強力なリスト操作ライブラリ群とでも思えば大体あってる、かな?C#とは縁のない人でも、便利に使えると思います。

LINQに対するアプローチは色々あるようですが、私はド直球にメソッド構文を実現することを目指しました。LINQの特徴として多く紹介されているクエリ構文を実現しようとすると、ただ煩雑になるだけで実用的なメリットはないからです。あくまで実用性を第一に考えました。そのため、無名関数をラムダ式風の文字列で記述可能にし、また、幾つかの短縮記法を設けてあります。

JavaScripterのための簡易説明

index付きのforeachが書け、IEでmapやfilterやreduceが使える。非破壊的なsortやremoveができる。DOMに対しても同じようにforeachやmapを適用できる。shuffle(ランダムに並びかえ)やdistinct(重複除去)など、便利メソッド集として使える。また、メソッドチェイン形式でシーケンスを加工していくことができる。具体的な用法は下の方で書くチュートリアルや即時実行可能な例題とセットのlinq.js Referenceを参考にしてください。

C#erのための簡易説明

文字列によるラムダ式の記述では、複数引数時でもカッコは必要ありません(つけても問題ありません)。引数がゼロ個の場合は、C#ではダミーとして「_」を使ったりしますが、linq.jsの場合は=>も書く必要がありません。例えばRepeatは「E.Repeat(null, 1000).Select("Math.random()")」などと記述することで、for文の代用として動かせたりします。また、ラムダ式には幾つかの省略記法を用意してありますが、それは後述します。

匿名型、的なものは、JavaScriptの文法に則りオブジェクトを作ることで解決します。つまりnewは必要なく{key:value,key:value}と書くことで匿名型と同様の動きをします。但しキーの省略は出来ません。また、ToDictionaryはありません、かわりにToObjectがあります。違いは、重複したキーが挿入されても例外を吐かず、上書きされます。もう一つ、基本的にメソッドは例外を吐きません。例えばFirst(predicate)で何も見つからなかった場合は例外ではなくnullを返します。唯一例外を吐くメソッドはSingle/SingleOrDefaultだけです。

Cast,OfType以外のメソッドは全て実装してあり、また、EqualityComparerを除けばオーバーロードも標準通りに従っています。実行時動作もC#と同じ、はずです(違う挙動を示す場合は教えてください)。標準メソッド以外に、「Cycle, From, RangeDown, ToInfinity, ToNegativeInfinity, Flatten, Pairwise, Scan, Slice, ZipWith, Shuffle, ToJSON, ToString, ToTable, Do, ForEach, Write, WriteLine, Force, Trace, TraceF」といったメソッドが定義されています。また、容易に新しいメソッドを拡張メソッドのように追加することが出来ます。

チュートリアル1. 生成

linq.jsを読み込むとLinqという名前とEという名前を占有します。EはLinq.Enumerableのショートカットになっています(jQueryなどの$と同じように、よく使うものなので短縮名を付けてあります)。linq.jsの動作は全てLinq.Objectの連鎖で実現されますが、そのLinq.Objectの生成をLinq.Enumerable以下のメソッドが担当します。生成メソッドは色々あるのですが、詳しくはリファレンスを参照ください、ということでここでは最も使うFromとRangeの解説をします。

var sequence = E.Range(1,10);
var array = sequence.ToArray(); // [1,2,3,4,5,6,7,8,9,10]
var sum = sequence.Sum(); // 55

Range(start,count)はstartの値からcountの個数分だけ整数を生成します。この例では1から10個分、10までです。E.Range(5,2)とすれば、5から2つ分で5,6が生成されることになります。Rangeメソッドが返すものは配列ではなくLinq.Objectなのでメソッドを繋げていくことが出来ます。また、宣言段階では値の生成は始まらず、ToArray(配列に変換)など具体的なオブジェクトへの変換メソッドや、Sumなどの数値集計メソッドが実行される時に値の生成が始まります。このことは以降、SelectやWhereメソッドの解説をする時にまた詳しく解説します。

var array = ["aaa",function(){return 3},{hoge:"tako"},1]; // 関数は除去されます
var seq1 = E.From(array);
var obj = {a:312,b:function(){return "hoge"},c:931}; // キーは.Key、値は.Valueで取り出す
var seq2 = E.From(obj);
var seq3 = E.From(10); // 数字、文字列は生成数1のLinq.Objectを生成します
var seq4 = E.From("hoge"); // つまりE.Repeat("hoge",1)と等しいです

配列やオブジェクトからLinq.Objectを生成するにはFromを使います。配列の場合は直感的に分かりやすくそのまんま順番通りに値を列挙していくのです、が、関数は自動で除去されます。つまりseq1は"aaa",{hoge:"tako"},1という値が生成されることになります。

オブジェクト/連想配列を入れると特殊な動作が入ります。値が関数のものは除去される、というのは配列と同じなのですが、プロパティをKeyに、値をValueに格納したオブジェクトが列挙されます。つまりseq2は{Key:a,Value:312},{Key:c,Value:931}という値が生成されることになります。なお、この動作はC#のDictionary(型付き連想配列)の動作(KeyValuePairが列挙される)を模しています。

Fromには数字や文字列を入れることも可能です。その場合は値を一度だけ送る、つまりRepeat(value,1)と等しくなります。また、オブジェクトに対してLINQを使いたいけれどKeyValuePairに変換したくない、という時はRepeat(obj,1)とすることで回避出来ます。Repeatの詳しい使い方はリファレンスを参照してください。C#標準と違い、回数指定を省くと無限リピートになります。

チュートリアル2. 射影とラムダ式

シーケンスの中身に関数を適用して変形する。他の言語ではmap関数として定義されていることが多いっぽいのですが、LINQではSelectメソッドがそれです。恐らく最も多用するメソッド。

E.Range(1,10).Select(function(i){ return i * 10})

1から10を生成→その数値を10倍。ということで結果は10,20,...,100です。勿論、文字列に変形させることも、オブジェクトに変形させることもできます。引数は、関数です。LINQはSelectに限らず無名関数を多用します、が、JavaScriptではfunction(){return}と書かなければならなくて記述が面倒くさい。ということで、文字列でC#のラムダ式のようなものを記述出来るようにしました。

// 普通の無名関数を用いる
E.Range(1, 10).Select(function(value, index) { return index + ':' + value });
// ラムダ式風の記述 "引数 => 式" として記述する
E.Range(1, 10).Select("value,index=>index+':'+value");
// 引数が一つの場合は、記号$を引数の変数として用いることができる
E.Range(1, 10).Select("i=>i*2");
E.Range(1, 10).Select("$*2");
// "x=>x"のような、自分自身を返す関数は使いたい場合、""で省略できる
E.Range(1, 10).Join(E.Range(8, 5), "x=>x", "x=>x", "outer,inner=>outer*inner");
E.Range(1, 10).Join(E.Range(8, 5), "", "", "outer,inner=>outer*inner");

ラムダ式は、=>の左が引数、右が式として評価されます。引数が一つの場合、=>を省くと$がその引数のかわりになります。プロパティへのアクセスも普通の引数を使うときと同じように「"$.hoge"」などでアクセスできます。更にJoinなど、"x=>x"を書く必要がある場合のための省略記法として""を用意してあります。つまり、""は"$"に等しく"x=>x"に等しくfunction(x){return x}に等しいことになります。

なお、ラムダ式をネストする際、前のラムダ式の変数は参照出来ません。何のこっちゃって感じですが、同梱のsample.htmの一番下のコードを見てください。その場合、無名関数を用いることで参照することができます。

チュートリアル3. 抽出・テスト

フィルタリングもSelectと同様に多用するメソッドです。他の言語ではfilterとして定義されていることが多いっぽいですが、LINQではWhereメソッドがそれです。

E.Range(1,10).Trace("Gen:")
  .Where("$%3==0").Trace("Filtered:")
  .Select("$*10")

1-10のうち3で割り切れる数のみを通して、10倍する。30,60,90が結果。で、挟んでいるTraceとは何かというと、そこを通っている値を画面に出力します(document.writeで書き出しているので、あまり実用性はありません、TraceFメソッドを用いるとFirebugのコンソールに出力できます)。リファレンスに設置してあるLINQ Padで試してみて欲しいのですが、Gen[1,2,3] -> Filtered[3] -> 30といった感じに値が流れている様子が確認出来ます。あくまで、E.Range(1,10)は10個の値を全て生成してからWhereに渡しているのではなく、一つ一つ生成して終わりまで通しています。

E.ToInfinity().Where("$*$*Math.PI>10000").First()

例えば、面積が10000を超える最初の半径は幾つ?といったように、どこまで値を生成すればいいのか分からない問題に効果を発揮します。なお、無限生成系は条件指定によっては永遠に止まらないものが出来てしまいますので注意してください。また、First(最初を取得)やTake(個数を指定して取得)は止まりますが、Last(最後のものを取得)やSingle(唯一の要素を取得)は絶対に止まりません。「最後」を得るために、「唯一」であることを確認するために生成が終わるまでループを回し続けるからです。

応用

以上で基本は終了です。同梱してあるsample.htmには、ひねくれた実例なども収録してあるのでご参照ください。また、リファレンスの上部に置いてあるLINQ Padはリアルタイムに動作を確認することが出来ますので、R.Range(0,10)辺りで数字を生成して遊んでみてください。例えば動画の例はランダムソート(笑)の検証と同じものになっています。0-10を生成してシャッフルして先頭を取り出したものを→キーを自身・値を自身にしてグループ分けして集計のキーをkey・集計の個数をcountというプロパティに格納したオブジェクトに変換して→keyの順番で昇順にソートして→書きだす。というコードです。

その他

C#のLINQの記述方法とほとんど同じなので、MSDNのLINQ to Objectsに関するドキュメントが参考になるかと思います。例えば標準クエリ演算子の概要とか。ネット上のLINQ to Objects用コードもそのまま動かせるはず、です。

実行効率……考えたら負けかな、と思っている。そんな派手なことやらなければ誤差範囲に収まる、といいなあ。この程度はGoogle Chrome先生が何とかしてくれる!あと、LINQ愛に突き動かされて書いたけれど、私はJavaScriptド素人なので(今までJavaScriptでまともな何かを書いたことがない) ソースへ、厳しい突っ込みが貰えたら嬉しいなあ、と思っています。内心、こういうコードでいいのか不安でいっぱいなので。

C#って日本のネット上では地味な存在だけど、linq.jsが架け橋になって、C#にも興味を持ってもらえたらなあ、なんて思いつつ、とりあえず、試してみてもらえると嬉しいです。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive