てくメモ

trivial な notes

【C#】クエリ式の推せるところ

いわゆるLINQにおいては、メソッドによる記述以外にクエリ式による記述を行うことができる。

var seq = Enumerable.Range(1, 10);
var index = 1;
var odd =
    from num in seq
    where num % 2 == 1
    select (index++,  num);

Console.WriteLine(string.Join(' ', odd));
// (1, 1) (2, 3) (3, 5) (4, 7) (5, 9)

LINQを学ぶ際におそらく一度は触れることになるので認知度は高そうな一方、実際に扱う際は(少なくとも LINQ to Objects においては)メソッド式の記述が支配的なのではないかと思う。


そんなクエリ式、個人的にはこの一点は推せるという点がある。
それは、3つ以上のシーケンスを同時に扱うのに直感的なこと。

例えば、直積。
今回は、6面ダイスを3つ振って合計値を得ることを考えてみる。(テーブルゲームでいうところの3D6)

var dice = Enumerable.Range(1, 6).ToArray(); // [1-6] のダイスのシーケンス
var rolls =
    from d1 in dice // ダイスをひとつ
    from d2 in dice // ふたつ
    from d3 in dice // みっつ用意して……
    select d1 + d2 + d3; // 足す

LINQの特徴たる宣言的な記述。
メソッド記述だともうちょっとノイズが入る(多分)。

さて、あとはしたいことに応じて加工。
今回はパーセンテージを取って文字列に出力してみる。

int[] sums = new int[6 * 3 + 1];
foreach (var r in rolls) sums[r]++;

StringBuilder sb = new();
for (int i = 0; i < sums.Length; i++)
{
    int count = sums[i];
    if (count <= 0) continue;

    sb.AppendLine($"{i,2}: {count / (6d * 6d * 6d),6:#0.00%} {new string('―', count)}");
}

Console.WriteLine(sb.ToString());
// 結果
//  3:  0.46% ―
//  4:  1.39% ―――
//  5:  2.78% ――――――
//  6:  4.63% ――――――――――
//  7:  6.94% ―――――――――――――――
//  8:  9.72% ―――――――――――――――――――――
//  9: 11.57% ―――――――――――――――――――――――――
// 10: 12.50% ―――――――――――――――――――――――――――
// 11: 12.50% ―――――――――――――――――――――――――――
// 12: 11.57% ―――――――――――――――――――――――――
// 13:  9.72% ―――――――――――――――――――――
// 14:  6.94% ―――――――――――――――
// 15:  4.63% ――――――――――
// 16:  2.78% ――――――
// 17:  1.39% ―――
// 18:  0.46% ―


今回は全部同じダイスを使ったけれど当然別のダイス(シーケンス)を使ったっていいし、今回は合計値を取ったけれど、別の形式に射影したっていい。


そんな直感的な記述を許してくれるクエリ式、内側で何をやってくれているの? というと、匿名型を使った多段SelectManyでよしなにしてくれている。

// コンパイラーによる多段 SelectMany
IEnumerable<int> rolls = dice.SelectMany((int d1) => dice, (int d1, int d2) => new { d1, d2 }).SelectMany(<>h__TransparentIdentifier0 => dice, (<>h__TransparentIdentifier0, int d3) => <>h__TransparentIdentifier0.d1 + <>h__TransparentIdentifier0.d2 + d3);


自動生成由来の多段処理やアロケーションもあり、そしてLINQということで、さすがに処理直書きよりは速度面で後れる。
今回は単純な多重ループと同等なので、それと較べてみる。

int[] sums = new int[6 * 3 + 1];
for (int d1 = 1; d1 <= 6; d1++)
{
    for (int d2 = 1; d2 <= 6; d2++)
    {
        for (int d3 = 1; d3 <= 6; d3++)
        {
            var sum = d1 + d2 + d3;
            sums[sum]++;
        }
    }
}
// 文字列関連部分は同じ
 Method |      Mean |    Error |    StdDev |   Gen0 | Allocated |
------- |----------:|---------:|----------:|-------:|----------:|
 ByLINQ | 11.026 μs | 1.582 μs | 0.0867 μs | 1.8921 |   5.81 KB |
 ByLoop |  6.316 μs | 6.782 μs | 0.3717 μs | 1.0376 |   3.19 KB |


当然差はある。

けれど、LINQの表現力は書き味がよく、そしてクエリ式は特定のケースではそれを後押ししてくれる。
というわけで(?)、クエリ式は推せる😎

終わり。