多重 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));

幾分かはマシに。それでもまだ、クエリ式のありがたみがわかります。
ここで書いたようなコードは、手動で等価なコードを書いてるんでまだマシで、ツールによる逆アセ結果は結構悲惨なことになります。難読化ツールなんて使わなくても十分難読。