C# の for 文を LINQ に置き換えたい

2021/09/02に公開約2,500字

LINQ を使い慣れると

LINQ は便利ですよね。通常のループを書く時には、データがループの先頭から最後まで行ったり来たりするので、それを追って視線も思考もループ内をグルグル回ります。ところが LINQ だと、データは上から下へ一直線に降りていくので、流れに従ってキーボードを走らせることができ、視線も上から下へと降りるだけです。もう全部 LINQ で書きたい、ループは foreach だけでいい、と思うくらい楽です。

そんな中、forwhile を使うと思うのです。「このデータ、ToList() したい! Sum() したい! Aggregate() したい!」と。もちろん、IEnumerable<T> を返すメソッドを作って yield return すればできるんですけれど、だったら素直に List.Add() の方が面倒がない。

拡張メソッドの作成

for に関してでも、単純なカウンタなら Enumerable.Range() で行けるけれども、完全に置き換えることはできない。じゃあ、というのでこういう拡張メソッドを作ってみました。

public static class MyLinq
{
	public static IEnumerable<T> Recur<T>(this T target, Func<T, T> func)
	{
		var p = target;
		while (true)
		{
			yield return p;
			p = func(p);
		}
	}
}

例えばこのように使います。

var data = 0.Rucur(i => i + 1).TakeWhile(i => i < 10);
foreach (var datum in data)
{
	Console.WriteLine(datum);
}

これは以下の二つと全く同じ動作をします。

for (int i = 0; i < 10; i++)
{
	Console.WriteLine(i);
}
var data = Enumerable.Range(0, 10);
foreach (var datum in data)
{
	Console.WriteLine(datum);
}

かえって複雑になってない?

その通りです。では次はどうでしょう?

for (int i = 2; i < 1000; i = i * i)
{
	Console.WriteLine(i);
}

これを Enumerable.Range() を使って処理するのは少し面倒くさそうです。Rucur を使うと

var data = 2.Rucur(i => i * i).TakeWhile(i => i < 1000);
foreach (var datum in data)
{
	Console.WriteLine(datum);
}

まったく同じ手順でできました。

フィボナッチ数列

それでは今度は、フィボナッチ数列を作ってみましょう。

var data = new[] { 0, 1 }
	.Recur(i => new[] { i[1], i[0] + i[1] })
	.Select(i => i[0])
	.TakeWhile(i => i < 100000000);

foreach (var datum in data)
{
	Console.WriteLine(datum);
}
0
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
10946
17711
28657
46368
75025
121393
196418
317811
514229
832040
1346269
2178309
3524578
5702887
9227465
14930352
24157817
39088169
63245986

ニュートン-ラフソン法

ニュートン・ラフソン法を用いて、3 の平方根を求めてみましょう。

var x = 3;
var result = ((double)x / 2)
	.Recur(i => (i + x / i) / 2)
	.ElementAt(10);

Console.WriteLine(result);
1.73205080756888

for ループは漸化式だった

ここまで見てきて気が付かれましたでしょうか?「フィボナッチ? ニュートン-ラフソン? それって漸化式を使うよね?」
Wikipedia - 漸化式

数学における漸化式(ぜんかしき、英: recurrence relation; 再帰関係式)は、各項がそれ以前の項の函数として定まるという意味で数列を再帰的に定める等式である。

「再帰的に定める」とは難しい言い回しですが、i = i + 1 と言うのを見てください。ほら、i によって次の i が決まっています。再帰してますね?
 for ループの条件は、初期値、終了条件、漸化式から成り立っていて、そうでない場合も初期値、終了条件、漸化式として一般化できます。初期値は与えられ、終了条件は TakeWhile() もしくは Take() で決定できるとするなら、あとは漸化式さえ実装すれば for ループを置き換えることができることになります。これが Recur の役目です。

執筆日: 2017/03/28

GitHubで編集を提案

Discussion

ログインするとコメントできます