多重 from の展開結果
もう1つ、↓にインスパイアされての話。
クエリ式で総当たり - NyaRuRuが地球にいたころ
上の記事で取り上げられてる話は、以下のような総当りで解く類の問題を LINQ で解こうという話。
Baker, Cooper, Fletcher, MillerとSmithは五階建てアパートの異なる階に住んでいる。Bakerは最上階に住むのではない。Cooperは最下階に住むのではない。Fletcherは最上階にも最下階にも住むのではない。MillerはCooperより上の階に住んでいる。SmithはFletcherの隣の階に住むのではない。FletcherはCooperの隣の階に住むのではない。それぞれはどの階に住んでいるか。
総当りの対象が5人いるんで、5重ループ(あるいは5重のfrom)が必要です。
まず、多少なりとも式の見栄えをよくするために、以下のような補助関数を用意。
// 1〜5 static IEnumerable<int> five = Enumerable.Range(1, 5); // x の要素に重複がないとき true static bool Distinct(params int[] x) { return x.Distinct().Count() == x.Length; } // x, y が隣り合う数字でないとき true static bool Discrete(int x, int y) { return Math.Abs(checked(x - y)) != 1; }
これを使ってクエリ式を書くと、↓みたいな感じ。
var answers1 = from baker in five from cooper in five from fletcher in five from miller in five from smith in five where Distinct(baker, cooper, fletcher, miller, smith) where baker != 5 where cooper != 1 where fletcher != 1 && fletcher != 5 where miller > cooper where Discrete(smith, fletcher) where Discrete(fletcher, cooper) select new { baker, cooper, fletcher, miller, smith };
で、今日は何が言いたいかというと、こいつが C# コンパイラにどう解釈されてるか。
クエリ式は、以下のように、メソッド呼び出しに変換されます。
// answers1 のクエリ式と等価なクエリ演算 var answers0 = five .SelectMany(x => five, (baker, cooper) => new { baker, cooper }) .SelectMany(x => five, (x, fletcher) => new { x, fletcher }) .SelectMany(x => five, (x, miller) => new { x, miller }) .SelectMany(x => five, (x, smith) => new { x, smith }) .Where(x => Distinct(x.x.x.x.baker, x.x.x.x.cooper, x.x.x.fletcher, x.x.miller, x.smith)) .Where(x => x.x.x.x.baker != 5) .Where(x => x.x.x.x.cooper != 1) .Where(x => x.x.x.fletcher != 1 && x.x.x.fletcher != 5) .Where(x => x.x.miller > x.x.x.x.cooper) .Where(x => Discrete(x.smith, x.x.x.fletcher)) .Where(x => Discrete(x.x.x.fletcher, x.x.x.x.cooper)) .Select(x => new { x.x.x.x.baker, x.x.x.x.cooper, x.x.x.fletcher, x.x.miller, x.smith });
この、元のクエリ式には出てこない妙な変数 x を透過識別子っていいます。x だらけで泣けてきます。こうなるのが嫌だから、C# 3.0にクエリ式が導入されたらしいです。
多少なりとも透過識別子を整理しなおせば、↓みたいになります。
// answers0 の透過識別子をちょっと整理 var answers01 = five .SelectMany(x => five, (baker, cooper) => new { baker, cooper }) .SelectMany(x => five, (x, fletcher) => new { x.baker, x.cooper, fletcher }) .SelectMany(x => five, (x, miller) => new { x.baker, x.cooper, x.fletcher, miller }) .SelectMany(x => five, (x, smith) => new { x.baker, x.cooper, x.fletcher, x.miller, smith }) .Where(x => Distinct(x.baker, x.cooper, x.fletcher, x.miller, x.smith)) .Where(x => x.baker != 5) .Where(x => x.cooper != 1) .Where(x => x.fletcher != 1 && x.fletcher != 5) .Where(x => x.miller > x.cooper) .Where(x => Discrete(x.smith, x.fletcher)) .Where(x => Discrete(x.fletcher, x.cooper));
幾分かはマシに。それでもまだ、クエリ式のありがたみがわかります。
ここで書いたようなコードは、手動で等価なコードを書いてるんでまだマシで、ツールによる逆アセ結果は結構悲惨なことになります。難読化ツールなんて使わなくても十分難読。