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#にも興味を持ってもらえたらなあ、なんて思いつつ、とりあえず、試してみてもらえると嬉しいです。

LINQPad for JavaScript

LINQPadのように気楽に入力して動作確認。JavaScript上で動かせると超お手軽で、確認が楽ちんでいいですね。LINQも正規表現のようにリスト操作用DSLと考えてみれば、こうやって即時評価できるチェッカーの便利さも納得。個人的にはこれだけでも作って良かったな、と思えています。

JavaScriptで一番使う列挙可能な物体といったらDOMなので、ちゃんとNodeListも扱えるようにしました。LINQPad上でもプロパティとキーを画面に出したりして動かせます。コード例は、outerHTMLがタグを吐くせいで、この画面上だとエラー出るのでWhereで除外しました。フィルタリングもできるよー、の例ということで。

現在、引数指定無し時の引数識別子は「x」ということにしているのだけど、$のほうが自然かなあ。どう思いますか? 変えるなら今のうちというか今を逃したら変えられないというか。$を使うとjQueryと衝突しそうなイメージが出るような気がして若干気が引ける(実際は全くそんなこたぁありません)。でもまあ、逆にメソッドチェーンだらけだし、見た目がjQueryっぽくなって受け入れられやすくなる、気もする。

※ DL数一桁で終わるであろう現実を見ていません

現在の作業状況としては、リファレンス書きが終わって、あとは英語でチュートリアル書くだけなんですが英語書けないので諦めようかと思案中。別にCodeplexだからって英語で書かなくてもいいぢゃない。諦めるを選べば、今週中に公開出来そうです。諦めないを選べば永遠に公開出来なさそうです。

31

今月はゲームについて絶対に書かない月間を完遂おめでとう!誰得!誰得!書かないついでに、ほとんどプレイもしてなかったのは普通にどうかと思う。来月からは普通のペースに戻したいかなあ、と思ってはいるんですがどうでしょう。先週から作り直してるjs用linqライブラリが出来上がったらパーッとやりたいのですけど。中途半端な状態だと気持ち悪くて。

一応出来上がりました。で、必死にリファレンス書いてます。英語で解説するのは無理だと諦めたので(笑) 例題コードをクリックするとその場で実行されるようにしました。実行結果はコードの真下に出てくる。これなら見たまんまな結果なので文字による解説が一つもなくても、それなりに分かりやすいでしょう。まあ、そもそもC#のLINQの丸々コピーなのでMSDNの親切なヘルプを見るのが一番なのですが。で、そうそう、一応はC#のLINQの丸々コピーになっているので、このリファレンスを見ることでC#のLINQのメソッドの動きの確認もしやすい、ような代物になればいいなぁ、と。

画像だとwidth640に縮めたので何がなんだか分かりませんね。カラムはメソッド名・戻り値・引数・コードです。フルサイズだとこんな感じ、な予定。進行状況1/3ぐらい。

ラムダ式モドキは、どうせ文字列から生成してるんだからと調子に乗って略記法を多めに搭載しました。引数一個なら=>を省くとxをデフォルトの引数変数名として使える、とか。なので本家LINQよりもサクサク書けます。サクサク書けるのはともかくとして実行効率は……そんなこと気にしたら負けかなと思っている。実用的にどうなのかはともかく、結構楽しく書き進められました(リファレンス書きは苦痛ですけど!) 作っただけで自分で使わないというのも寂しいので、これが終わったらFirefoxのアドオン作成でもして、それに使おうかなあ……。

JavaScriptでLINQ

あまりJavaScriptっぽく見えませんが、JavaScript用のLINQライブラリを制作中です。1月頃に一応完成はしていたのだけど、あまり良い出来ではなかったので放置していました。けど、ふと思い立って昨日から改修を始めました。

  • C#のLINQとの95%の互換性(メソッドオーバーロードの関係上、一部が無理)
  • Achiralからパク……参考にさせていただいた標準LINQ外の拡張メソッド群
  • ラムダ式もどきによる簡潔な記述
  • C#の拡張メソッドのように簡単に俺俺メソッドを追加可能

という4つの特徴を持ちます予定。4番目は微妙かも。yield returnがないのでコードがゴチャついちゃってて。C#にないFromメソッドは、Array.prototypeを拡張するのはお行儀悪いので、配列をメソッドチェーン可能にさせるための苦肉の策。若干面倒くさいですが勘弁してやってください。Fromは連想配列を放り込んでも動作します。その場合、以降キーは.Key、値は.Valueで取り出すことになります。この辺もC#を模してます。あと、TraceメソッドはFirebugのコンソールに流せるようにしたりする予定。

この手のものは珍しくないし、LINQ風JavaScriptライブラリもそれなりな数があるんですが、ラムダ式もどきで簡潔に記述出来ることもあってわりかし使い勝手が良いほうに入るんじゃないかと思います。実際問題何が便利なのか、というと、IEでfilterやmapやreduceが使えるとでも思ってくれればどーでしょう。もしくはPerl のリスト操作を Ruby 風に的な。

4月中ごろまでには公開出来たらいいなあ……。

追記:完成しました - neue cc - linq.js - JavaScript用LINQライブラリ

Scan?

いつかの記事が、何とNyaRuRuさんに取り上げて頂いて 前後の値も利用したシーケンス処理 - NyaRuRuの日記 嬉しさのあまり卒倒しつつ恥ずかしさでいっぱいな今日この頃。

int[] array = { 1, 2, 4, 4, 3, 3, 4, 0, 0 };

var result = array
    .OfType<Nullable<int>>()
    .Scan(new Tuple<int?, int?>(), (t, x) => Make.Tuple(t.Item2, x))
    .Skip(1)
    .Where(t => t.Item1 != t.Item2)
    .Select(t => t.Item2.Value);

Pairwiseを使う、などの発想が全くなかったので確認のためAchiralを使って書き直し。ただ、何だか怪しい。Scanを使うと、seedの分が一つ余計なのでSkip(1)はしょうがない、はず。一つズらすのでペアが出来ないところをNullableで補うのも必要、な、はず。はずなんだけど何だか違和感が拭えない。うーん。

intという、型を明示していくのが非常に書きにくい。これが違和感の元なのかな、普通にLINQで書くときはあまりないシチュエーションなので。ただ、こればっかりはしょうがない気がする。Nullableでなくても、空のTuple作るときに型が必要だから。んー、そもそも、Nullableにしなければもう少し綺麗になるような。ScanのseedをMake.Tuple(int.MinValue,int.MinValue)にでもすれば……。でもそれはそれで納得が行かない気がする。うーん、すっきりしない。

C#でカリー化

LINQハァハァ→関数型言語?→Haskell!という感じに辿ってHaskellを勉強中なので、カリー化について。Haskell、の前にまずはC#で考える。例えば、たまーに見かける「a => b => c => a + b + c」のようなものがパッと見意味分からない。Haskellでも型定義Int->Int->Intみたいことやるので、それと同じなわけっぽいですけれど。

ゆっくり分解すると、「=>」の左が引数、右が戻り値なのでa =>( b => c => a + b + c) つまり Func<int,???>。???の部分もカッコでくくって b =>( c => a + b + c) つまりFunc<int,Func<int,???>>。c=>a+b+cは、見たまんまなのでFunc<int,Func<int,Func<int,int>>>ということになる。

冷静に解きほぐせばそんな難しくない。というわけで準備運動が済んだので、カリー化関数を作ろう。

// カリー化する関数
static Func<T, Func<U, V>> Currying<T, U, V>(Func<T, U, V> func)
{
    return t => u => func(t, u);
}

// 非カリー化する関数
static Func<T, U, V> UnCurrying<T, U, V>(Func<T, Func<U, V>> func)
{
    return (t, u) => func(t)(u);
}

// 例として使うx+yを返す関数
static int Sum(int x, int y)
{
    return x + y;
}

static void Main()
{
    int num1 = Sum(3, 5); // 普通に、8

    // Sumをカリー化する、関数はデリゲートに包む必要がある
    var CurriedSum = Currying((Func<int, int, int>)Sum);
    int num2 = CurriedSum(3)(5); // 当然、8

    var BindedSum = CurriedSum(3); // 3を部分適用する
    int num3 = BindedSum(5); // 勿論、8

    var UnCurriedSum = UnCurrying(CurriedSum); // 非カリー化
    int num4 = UnCurriedSum(3, 5); // 当然、Sum関数と一緒
}

多分、あってる、と思いたい。カリー化とは大雑把に言って「f(x,y) = g(x)(y)」ということのようなので、そうなるようバラしたり戻したり。そういえばで気付いたのですが、関数/ラムダ式をデリゲートに包む方法は

Func<int> test1 = () => 1;
var test2 = (Func<int>)(() => 2);
var test3 = new Func<int>(() => 3);

意外とバリエーションがある、気がする。ようは省略出来るだけでしょって話っぽいけど。単独で定義するときは1番を用いるかなー。癖でとりあえずvar、って書いてから、あー、これはnewするのダルいケースだった、varじゃなくてちゃんと書かなきゃ、と後ろに戻ったりするのがよくあって笑えない。

クロージャ

関数を返す関数繋がりでついでに、関数型言語って何がすごいんですか - Gemmaの日記からロケットのコードのC#版を。

 static void Main()
{
    Func<int, Action> Rocket = n => () => Console.WriteLine((n > 0) ? (n--).ToString() : "liftoff");
    var F = Rocket(3);
    var G = Rocket(3);
    F(); F(); F(); F(); // 行数使うのもアレなので横に並べます
    G(); G();
}

中々なスッキリ具合。function retrunが必要ない、というのがC#ラムダ式の強みですねえ。JavaScriptは(IEのせいもあって)function{return}が必須なのがカッタルイ。そんなことを思うと、C#はとても身軽で、個人的にはある意味とてもLightweightだと感じてしまったりする。

インテリセンスがりがり、コードスニペットがりがり、コードフォーマッタがりがりな上に乗っかっているので、軽快というか、Lightweight Languageが短距離走選手のようだとしたら、VS2008+C#は重武装+ロケットブースターで、それ自体は鈍重であっても、結果的にブースター付きのは強烈なスピードだよね、みたいな。軽快に書き進められるの自分の力じゃなくてVisualStudioの力でしょ、と思わないときもないけれど。

LINQとeach_sliceと匿名Enum

ToLookupで思ったのは、匿名Enumが欲しい。わざわざ外に定義するほどでもないけれど、ただの文字列で区分けするのは嫌だ。なんて思ったのは、以前ジオメトリのランキングを引っ張ってくる際にGroupByを使った時に思ったんだった。それにしても、改めて眺めてみると酷い、Firstの連発とか。しかし、昔とは違うんです、昔とは。冷静に問題を考えればきっと分かる。

ようするに配列を3つ刻みで分割したい。{3, 4, 631, 671, 7, 5, 82, 1, 2}とあったら、{[3,4,631],[671,7,5],[82,1,2]}に分けたい。一次元→二次元、ということかな?phpのarray_chunkやRubyのEnumerable#each_sliceがそれに相当する、っぽい。

しかし延々と考えてもさっぱり思い浮かばない。自分で思い浮かばない時は検索しよう、と検索したらすぐ出てきた。Eric White's Blog : Chunking a Collection into Groups of Three。 3つ区切りでグループ分け、とはまさに考えている問題と同じ話。ただ、あの、このFirstの連打は私とやってること同じじゃん。

私のようなタコならともかく、他の人も似たようなことやってるし、もしくは拡張メソッドでループ回している例しか見つからないから、しょうがないかな、と諦め。きれない。ようはSkip().First()にせよFirst(predicate)にせよ、IEnumerable<T>状態だからいけない。何らかのキーで直接アクセス出来ればいいわけですよね? お、ToLookupぢゃん!でも今回は中に入るのが一個であること確定なので、ToDictionaryが良いですね。そして、ふふ、滅多に出番のないGroupByのresultSelectorを使うときがついにきたようだ……。

// 3つ区切りで、[商品名,値段,売ってる場所]となっている配列を分解したい
string[] array = { "大根", "100", "八百屋", "豚肉", "300", "肉屋", "イカ", "150", "魚屋" };

// enumモドキの匿名型
var Key = new
{
    Name = Guid.NewGuid(),
    Price = Guid.NewGuid(),
    Shop = Guid.NewGuid()
};

var result = array.Select((value, index) => new
    {
        value,
        chunk = index / 3,
        attr = (index % 3 == 0) ? Key.Name :
               (index % 3 == 1) ? Key.Price :
               Key.Shop
    }).GroupBy(t => t.chunk, t => t, (k, g) => g.ToDictionary(t => t.attr, t => t.value))
    .Select(d => new
    {
        Name = d[Key.Name],
        Price = d[Key.Price],
        Shop = d[Key.Shop]
    });

できたー。最初のSelect時にバラしているので行数がSkip().First()より増えているのは突っ込みどころだけど、満足。あと、元の配列がきっかり3で割りきれないと、Dictionaryなので存在しないキーにアクセスしました例外が出て死んでしまいます。対策としてはToLookupにして[Key.Name].FirstOrDefault()を使う、というのがすぐに浮かんだけど、スッキリしないのは否めない。ていうかその辺も踏まえてもSkip().FirstOrDefault()のほうが賢いやり方な気がする……。

で、冒頭の匿名Enum云々は、匿名型を使って実現?してみました。値は、絶対に被らないもの、ということでGUIDを使ってみた。ただ、定義時に全部にGuid.NewGuid()というのがダルい。というか若干ヤケくそ気味なのは否めない。こちらも上手いやり方が思い浮かばず、ってそればっか。そもそもEnumモドキなら、intでいいし、もしくは"商品名"とか文字列を使えば普通に便利なので、GUIDは全く意味ないですね。しょうもな。というわけで、実際に使う場合(あるかなあ?)はGUIDじゃなくて文字列でやります……。

回数サンドイッチ

class Program
{
    static void Main()
    {
        // 何でもいい配列
        var array = Enumerable.Repeat("a", 10);

        // foreachで素直っぽくindexを付ける
        foreach (var item in array.Select((value, index) => new { value, index }))
        {
            Console.WriteLine("{0}:{1}", item.index, item.value);
        }

        // 拡張メソッドにしちゃう
        foreach (var item in array.WithIndex())
        {
            Console.WriteLine("{0}:{1}", item.Key, item.Value);
        }

        // foreachで使う場合は普通に外側にcount用の変数置いた方が……
        // indexの取れない拡張メソッド(ToLookupとか)へのチェイン時には便利に使えるかも
        var splittedArray = array.WithIndex().ToLookup(kvp => kvp.Key < 5, kvp => kvp.Value);
    }
}

public static class ExtMethods
{
    public static IEnumerable<KeyValuePair<int, T>> WithIndex<T>(this IEnumerable<T> source)
    {
        int index = 0;
        foreach (var item in source)
        {
            yield return new KeyValuePair<int, T>(index++, item);
        }
    }
}

最近は、どこにでもあるゲーム雑感サイト→どこにでもあるプログラミング雑感サイト、に無理矢理シフトしようとして浮ついた無理無理感が漂っていますが、別に特にシフトしたいわけでもなく、せっかくなので3月は集中的に書いてみようかな、と思っただけですが空気的に不評な雰囲気を感じちゃってたりしなかったりは、とりあえず無視黙殺で進めようかと思うこの頃ですがいかがお過ごしでしょうか。

前々回、前回からまだ続いて、Selectでindexを取る話。[C#]何度目の動きを見て、便利便利と思ったので簡単に使えるように拡張メソッドに放り込んでみた。KeyValuePairなんて名前じゃなくてIndexValuePairが良いんですが(笑) あるものは、そのまんま使うということで。

それ自体は別にforeachの外側にカウント用の変数置けばいいぢゃーん、という気がしなくもなくて困ったので、言い訳として、幾つかのメソッドがインデックス取れないから中間に挟む用として便利!と思うことにした。SelectとかWhereとか、大抵のものはFunc<T,int,TResult>も用意されているんだけどね。ToLookupとか、一部のものには無いんだね。

C# LINQで左外部自己結合

static void Main(string[] args)
{
    // 連続して同じ値が来る箇所だけを省いて取得する
    // この場合だと1,2,4,3,4,0が取れることを目指す
    int[] array = { 1, 2, 4, 4, 3, 3, 4, 0, 0 };
    var arrayWithIndex = array.Select((value, index) => new { value, index });

    var result =
        from orig in arrayWithIndex
        join alias in arrayWithIndex on orig.index equals alias.index - 1 into _
        from alias in _.DefaultIfEmpty()
        where alias == null || orig.value != alias.value
        select orig.value;
}

前回に引き続いてindex生成してゴニョゴニョするネタを考えたい。というわけで、SQL的な自己結合。これで前後の値との比較が可能になるわけですね! 自己結合自体は普通にjoinで同じソースを置くだけ。例では、「連続して同じ値が入っている箇所」を省くため、インデックスを1つずらして結合。ただ、普通に結合すると最後の値が無いので、結合から抜け落ちてしまう。というわけで、内部結合ではなく外部結合にする。外部結合はDefaultIfEmptyを使って、MSDNに記事があるのをそのまんまな方向で。

var list = new List<int>();
list.Add(array[0]);
for (int i = 1; i < array.Length; i++)
{
	if (array[i] != array[i - 1]) list.Add(array[i]);
} 

……でも結局、こんな例ならばList使った方が遥かにスッキリなのであった。意味ないねー。とても。少しはまともな例題が考え出せやしないものなのかしらん。

リストの分割

// リスト自体は何だっていいので適当に生成
var list = Enumerable.Repeat("a", 50);
// 25以下をtrue、それ以上をfalseとするkeyでグループ分け
var result = list
	.Select((value, index) => new { value, index })
	.GroupBy(t => t.index <= 25, t => t.value);
// 多分、こんな感じに取り出す……?
var first = result.Single(g => g.Key == true);
var second = result.Single(g => g.Key == false);

2ch見てたら、要素数50のリストを0-25と26-50に分割するのどーするの、という質問が出てたのでごにゃごにゃ考えた。最初、list.GroupBy(i => i < 25);なんて答えたのだけど、即座に値じゃなくて添え字で分けたいんでしょーがボケ、と突っ込みが入ったので書き換えたのが↑のもの。微妙な長さと無駄手間感が何とも言えない、ただのLINQ遊びでしかない状態になってしまった。うーん。これはこれで面白いと思うのだけど。ただ、もう少し短くできないかな……?グループ化が挟まると、その後がどうしても長ったらしく。ていうか、あと、trueとfalseしかないんだから、First()とLast()でいいよね、という話ではある。

// 条件ばかりが無駄に多くて限りなく微妙
var alphabet = Enumerable.Range('A', 'z' + 1 - 'A')
	.Where(c => c <= 'Z' || c >= 'a')
	.GroupBy(c => c <= 'Z', c => (char)c);
// ていうか、意味ないよね、これ、全く
var upper = alphabet.First();
var lower = alphabet.Last();

何だか悔しいので、もう少し考えてみた。例えばアルファベットの大文字と小文字とか!……意味ない。死ぬほど意味がない。少なくとも、これだけを考えるならEnumerable.Range('a', 26).Select(i => ((char)i).ToString());でいいし。ダメだこりゃ。

追記

あ、違う、こういう場合はGroupByじゃなくてToLookup使えばいいんだった。GroupByと違ってキーでアクセス出来るようになるから、SingleだのFirstだのといった微妙なアクセスじゃなく(しかも、これらを使うと毎回走査してるってことだよね!) result[true]とかalphabet[true]とかでアクセス出来る。これで遙かにスッキリ。Lookupは素晴らしい。けど、ついつい忘れてしまう。

var result = list
	.Select((value, index) => new { value, index })
	.ToLookup(t => (t.index <= 25) ? "以下" : "より上", t => t.value);
var ika = result["以下"]; // こんな感じで
var tako = result["より上"]; // 取り出せるわけです!

これならすっきり。めでたしめでたし。キーは日本語でもboolでもenumでも好きなモノを使うといいさあー。

追追記

さっそく Enumerable.ToLookup が役立った - NyaRuRuの日記
つまりこういうことなのであった。間違いなく昔読んだはずなのに、すっかり忘れていたという事実が何よりも悲しい。あと、ToLookupで検索しても全然引っかからないというのが、限りなく人気無いメソッドのようで悲しい。私も忘れてたけどね!

C#とLINQでFizzBuzz

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        // FizzBuzzの無限リスト
        var FizzBuzz = Enumerable.Range(0, int.MaxValue).Select(i =>
            (i % 15 == 0) ? "FizzBuzz" :
            (i % 3 == 0) ? "Fizz" :
            (i % 5 == 0) ? "Buzz" :
            i.ToString());

        // Skipが起点、Takeが表示数に対応する
        FizzBuzz.Skip(1).Take(100).ForEach((s, i) => Console.WriteLine("{0,3}:{1}", i + 1, s));
    }
}

public static class ExtensionMethods
{
    // indexが取れるForEach
    public static void ForEach<T>(this IEnumerable<T> source, Action<T, int> action)
    {
        int index = 0;
        foreach (var item in source) { action(item, index++); }
    }
}

何だかふと思って、今更ながらにanarchy golf - FizzBuzzをやってた。とりあえずC#でSize 123で同率一位達成なので満足です。以上。で終わるのもアレなので、記念にFizzBuzzをきちんと書くならどうなるかなあ、と考えてみた。

C#3.0だとLINQを使うのがきっとスマートです、よね。無限リストを作ってやるのが一番、かな。Skipがstart、Takeがcountに相当するってのが何だか面白い。しかし毎度ながらEnumerable.Range(0,int.MaxValue)という表記が冗長に過ぎてげんなり。あとIEnumerableにForEachが用意されてないのもなあ。ToList()は色々と違うと思うので、本当に何とかして欲しい。

UbiquityとJavaScript

gist: 46381 — GitHub

あまりの酷いJavaScript素人っぷりに嫌気がさしたので、JavaScript 第5版を読みました。とりあえず9章まで。というわけで、それをもとにして書きなおしてみました。functionがコンストラクタなんだねー、とかprototypeとか。そんな感想を反映。前よりは遥かにマシ、だとは思います。

んで、機能的には大幅アップです。modeを確定させる前は全部のモードから表示。modeを確定させたら、前回はプレビュー領域が余りまくりで勿体なかったので、前後3人のスコアを同時表示に変更。これでわりと実用的になってきたんじゃあないかと思われますがどうでしょう?若干、というかかなり動作が怪しいー通信しすぎー、一覧表示でてこねーぞごるぁー、ってところがあるので、追々直します。

ジオメトリランキング表示Ubiquityコマンド

gist: 46381 — GitHub

ちょっと前からゴニョゴニョと弄り中のジオメトリウォーズ2のランキング。実用的なものを、と考えた結果、FirefoxのアドオンUbiquityのコマンドに仕上げてみました。Ubiquityをインストールした状態で上記GitHubのサイトに飛ぶと上部にSubscribeするかどうかの情報バーが出るので、Subscribeすればコマンドのインストールが完了します。

使い方

Ubiquityを起動し(デフォルトではCtrl+Space)「gwre2 GAMERTAG mode MODE」とコマンドを入力するとプレビュー領域にランキングとスコアが表示され、そのままエンターキーを押すと、テキストキャレットの位置にスコアが挿入されます。例えば「gwre2 SeaWeeze mode Deadline」と入力すると「Rank:143|Score:45218795」がプレビュー領域に、エンターを押すことで「45218795」が挿入される、というわけです。

入力補完が効くので実際の入力は「gw SeaWeeze mode d」だけでOKです。また、modeを入力すると下にその他の候補が出るので、下カーソルを押すことでスムーズに他のモードのランク/スコアを見ることが可能です。残念ながら今のところUbiquityの仕様で5つまでしか出せませんけど(モードは6つあるので一つ足りない、ので基本的にSequenceが場外)

ネットを見ててゲーマータグがあると、この人のジオメトリのスコアはどのぐらいだろうなあ、とかすぐ思ってしまう人にお薦め。定期的に自分のスコアを張り付けている人にもお薦め。もうメモらなくても済む!

コード品質

ソースコード丸見え!見ちゃいやあ。そうです、品質最悪です。実のところjavascriptが全然分からないのです。そしてjQueryは初めて触った、ので全く分からない。Ubiquityも初めて、当然さっぱり。分からないx3により酷いことになっています。いやあ、困った困った。誰か改良してくれれば……。というのはともかく、明らかに酷いのは分かっているので、ちょくちょくと改良していこうとは思っています。

modeを入力しない状態だとpreview領域に全部のスコアとランクを出す。というほうが便利ですよねー。ただそうなるとサーバーにリクエスト飛ばしまくることになるので(単純にx6というだけじゃなくて、ゲーマータグの確定前にもリクエストが飛ぶので、x15ぐらいになりそう)どうかなー、と思ったんですが、どーなんでしょうかね。やっぱ不便なので、全部のスコアとランクが出た方がいいには違いないんですけどねえ。

リーダーボード分解

 static void Main(string[] args)
 {
     var service = new XBLLeaderboardSvc.ServiceSoapClient("ServiceSoap");
     var result = service.getLeaderboardNearGamertag(2, 4, "SeaWeeze", 10)
         .Skip(1) // 最初の1個目は値の個数なのでスキップ
         .Select((value, index) => new
         {
             value = value,
             index = index / 3,
             attr = (index % 3 == 0) ? "Rank" :
                    (index % 3 == 1) ? "Tag" :
                    "Score"
         })
         .GroupBy(t => t.index, t => new { t.value, t.attr })
         .Select(g => new
         {
             tag = g.First(t => t.attr == "Tag").value,
             rank = int.Parse(g.First(t => t.attr == "Rank").value),
             score = int.Parse(g.First(t => t.attr == "Score").value)
         });
 }

今年はプログラミング分を強化したいので、コードも張り付けていこう。勿論恥ずかしいけれど、照れてても成長に繋がらないと思うので、積極的に行きたいです。言語は基本的にC#3.0。というわけで、前回、GeometryWarsのLeaderboardの値を取得するの続き。ただの文字列配列で帰ってくるので分解しなきゃねー、ということで分解、まで。この後の予定は未定。ゆったりとSilverlightで簡単なグラフでも作ってから本格的に考えようかな―、と思っています。

サービス参照を追加するだけで、メソッド一発(getLeaderboardNearGamertag())で軽く取得出来る。これは楽ちん。配列はLINQを使って分解することにしました。3つ区切りなので3で割ってインデックスと属性振ってグループ化してタグから匿名型作って……。何かイマイチ。attrが野暮ったい。この辺、何とかしたい気はとてもする。

と、進捗的にはつまり全く進んでないということですね! いや、ここずっとexception conflictやってるし……。

GeometryWars Leaderboard API

Xbox360がネットランキングを標準搭載したように、次世代XboxはWeb上からランキングデータを取得できるAPIを標準公開して欲しい。いや、公開すべき。そうすれば、あんなこともこんなことも、夢が膨らむ。残念ながら現行世代のXbox360はム・リ。ではあるものの、最近ではウェブ上からランキングを参照出来るゲームも多くなってきた、ということは需要の後押しがある、ということで次世代Xboxでの搭載は夢ではなさそう。HALO3のマッチングシステムと共に標準搭載の勢いでよろしくお願いしたい。

で、本題。ジオメトリ2もメーカー側のランキングや、ランキング付きゲーマーカードのようにウェブ上に公開可能な仕組みは用意されていた、けど、一般人が弄れるAPIは公開されていなかった。しかしBizarre Creationsはついにやってくれた。さらっとBBS上に公開したよー、とのお知らせが!

というわけで、そのBizarre CreationsのXBLAタイトル用のLeadboard取得WebServiceの解説。呼び出し方法が数字直打ちで分かりづらければ、帰ってくる値も素の配列という分かりづらさなので、私が調べた範囲で簡単に解説します。いや、数字ぽてぽて打って確認するだけで別に手間でもなんでもないですけどね。

getLeaderboardは順位のランキング。Overallって奴ですね。パラメータはgameIdとleaderboardIdはあとで解説します。rankToStartで開始する順位を、numToShowで取得する量を決めます。残念ながらrankToStartは901まで、numToShowも有効なのは100まで、ということで1000位までしか取得できません。十分ですけど。

getLeaderboardNearGamertagは任意のゲーマータグの人の前後のランキングを取得する。gamerTagはゲーマータグ、numToShowは前後の量。こちらのnumToShowは有効な値は101までになっています。自分の前に50人+自分の後ろに50人、ということですね。

さて、gameIdですけど、0はGWRE1、1はBoomBoomRocket、2はGWRE2。GWRE1のleaderboardIdは、0がEVOLVED、1がRETRO。BoomBoomRocketは割愛(笑) GWRE2は、0=Deadline,1=Evolved,2=Sequence,3=Waves,4=Pacifism,5=Kingでした。変な並び順ですけど気にしないことにしましょう。いや、めっちゃ気になるけど。

で、取得できるxmlは全部stringというそっけなさ。一番目が取得したデータの個数、それ以降は3つ区切りで「順位、ゲーマータグ、スコア」になっているようです。

ついでにRESTでの取得。http://www.bizarrecreations.com/XBLLeaderboardSvc/Service.asmx/getLeaderboardNearGamertag?gameId=2&leaderboardId=4&gamerTag=SeaWeeze&numToShow=11、このURLはgameIdが2なのでGWRE2、leaderboardIdが4なのでPacifism、gamerTagがSeaWeezeなので、まあ、私ですね、で、numToShowが11なので前後5人ずつのランキングが取得出来ます。

いやあ、素晴らしいですね。色々と素っ気なさ過ぎじゃね?って気がしなくもないですが、それが非公開のサービスを一般向けに公開してあげる、という優しさに感じられて嬉しかったりして。実際問題、こんなに大盤振る舞いで公開してくれている会社なんて無いですからねえ。今後もBizarreに続いてくれると(Bungieぐらいかな、もしやるとしても)嬉しいし、その勢いで次世代Xboxでは公開が当然、みたいな形になってくれれば、なお嬉しい。

そのためにも、せっかく公開してくれたこのサービスを使って何かを作らなければ!と思うんだけど、いざ何かっていうと何も思い浮かばない。プログラム流用で、ランキングが100落ちる度にTwitterに投稿するツールとか、どうでもいいネタしか浮かばない。というわけで、何かアイディアがあればください。

Prev |

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(.NET)
April 2011
|
July 2027

X:@neuecc GitHub:neuecc

Archive