1. はじめに
純粋関数とは、同じ入力に対して常に同じ結果を返し、外部に何の副作用も与えない関数です。簡単に言うと:純粋関数は周りを「汚さない」し、外からの影響も受けない。
関数が純粋と見なされる条件:
- 入力引数に基づいてだけ値を計算する。
- 外部の変数やプログラムの状態を変更しない。
- 変化しうる外部の変数や状態に依存しない。
純粋性の二つの黄金律
- 決定性:同じ入力は同じ出力。
- 副作用なし:関数は自身の外側の何も変更しない:ファイルも、グローバル変数も、ユーザーインターフェイスも(こんにちは、 Console.WriteLine)。
宗教ではなく常識
これは学術的なこだわりに見えるかもしれませんが、実践では逆に有益です:
- 純粋関数は予測可能です。テストが簡単 — ある引数で呼べば決まった結果が返るだけ。
- 純粋関数はコード内で自由に移動でき、どこかが壊れる心配が少ない。
- マルチコアの世界では、純粋関数なら競合を気にせず並列実行できる安心感がある。
2. 純粋関数と非純粋関数の例
純粋関数:すべてが予測可能
// 純粋関数: 外部を触らない
int Add(int a, int b)
{
return a + b;
}
int Square(int x)
{
return x * x;
}
Add(2, 3) を何度呼んでも常に 5 が返る。退屈だけど安全。
非純粋関数:ルールを破る
// 純粋性を破る: 外部状態(static 変数)に依存
int counter = 0;
int Increase()
{
counter++;
return counter;
}
ここでは Increase() が呼ばれるたびに異なる値を返す — つまり決定性がない。
// 純粋性を破る: 外部効果(画面への出力)を行う
int AddAndPrint(int a, int b)
{
int sum = a + b;
Console.WriteLine(sum); // 副作用!
return sum;
}
乱数や時間については?
DateTime.Now や Random を使う関数は純粋ではない:
// 純粋ではない!
int GetRandomNumber()
{
return new Random().Next();
}
違いの表
| 特徴 | 純粋関数 | 非純粋関数 |
|---|---|---|
| 同じ引数で常に同じ結果 | はい | いいえ |
| 副作用 | なし | あり |
| 外部状態への依存 | なし | あり |
3. データのイミュータビリティ:理論と実践
イミュータビリティ(immutability) は、オブジェクトが作成後に変更できないという考え方です。新しい値が必要なら、新しいオブジェクトを作る。
なぜ重要か?
- アプリケーションが誤ってデータを書き換えてしまうことに強くなる。
- 「誰かがどこかで勝手に変更した」みたいな隠れたバグが減る。
- イミュータビリティは多くの自動最適化や並列計算の基盤になる。
C# のシンプルな例
.NET のイミュータブル型
C# の文字列(string)はイミュータブルです!string.Concat(s, "world") を呼ぶたびに新しい文字列が作られます。
string s = "Hello";
string t = s;
s = s + " World";
Console.WriteLine(t); // t == "Hello"
配列とコレクションはデフォルトでミュータブル
int[] numbers = { 1, 2, 3 };
numbers[0] = 42; // 配列が変更された!
イミュータビリティをイメージで:コードの書き換え
既存のオブジェクト/値を変更する代わりに、新しいものを返す:
// これをする代わりに:
void AddToList(List<int> list, int value)
{
list.Add(value); // ミューテートする!
}
// こっちのほうが良い:
List<int> AddToList(List<int> list, int value)
{
var newList = new List<int>(list) { value }; // 新しいリスト
return newList;
}
イラスト:変える vs 変えない
flowchart LR
A[元のオブジェクト] --"ミューテーション"--> B[同じオブジェクトだが中身が変わる]
A --"イミュータビリティ"--> C[新しいオブジェクト]
4. 実際の C# コードでの利点
- 現代の C# ライブラリ(LINQ、Entity Framework、ASP.NET Core など)は純粋関数とイミュータビリティを活用する傾向にある。
- イミュータビリティはどこかで重要な値が上書きされて消える、という「魔法の」バグを減らす。
- 純粋関数はユニットテストを簡単にする — テストでは入力と出力だけを気にすればよい。
例:文字列操作
string s = "Hello";
string newS = s.Replace("H", "J"); // s はそのまま "Hello"; newS は "Jello"
例:LINQ とコレクション
Where、Select などは元のコレクションを触らずに新しいコレクションを返す。
var numbers = new List<int> { 1, 2, 3, 4 };
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
// numbers は変更されていない!
実務例:イミュータブルな設定オブジェクト
多くのモダンな .NET API は設定にイミュータブルなオブジェクトを使う。例えば JsonSerializerOptions:
var options = new JsonSerializerOptions
{
WriteIndented = true
};
// このオブジェクトは「途中で変わらない」ので信頼性が上がる。
5. 典型的なミスと落とし穴
滑りやすいのは、「純粋」と思って書いたコードが実はデータをミューテートしている場合。
コレクションでフィルタをかけるつもりが、元のリストをいじってしまうことがよくある。
あるいは、List<T>.Add がその場でオブジェクトを変えることを忘れているケース。
厄介な例:
List<int> DoubleTheNumbers(List<int> xs)
{
// NG! 元のリストをミューテートして、同じオブジェクトを返している。
foreach (var i in xs)
xs.Add(i * 2);
return xs;
}
このコードは実行時に InvalidOperationException すら投げるだろう — ループ中にコレクションを変更しているからだ。ミューテーションの古典的な罠。
正しいやり方:
List<int> DoubleTheNumbers(List<int> xs)
{
var newList = new List<int>(xs.Select(x => x * 2));
return newList;
}
GO TO FULL VERSION