CodeGym /コース /C# SELF /純粋関数とイミュータビリティ

純粋関数とイミュータビリティ

C# SELF
レベル 51 , レッスン 1
使用可能

1. はじめに

純粋関数とは、同じ入力に対して常に同じ結果を返し、外部に何の副作用も与えない関数です。簡単に言うと:純粋関数は周りを「汚さない」し、外からの影響も受けない。

関数が純粋と見なされる条件:

  • 入力引数に基づいてだけ値を計算する。
  • 外部の変数やプログラムの状態を変更しない。
  • 変化しうる外部の変数や状態に依存しない。

純粋性の二つの黄金律

  1. 決定性:同じ入力は同じ出力。
  2. 副作用なし:関数は自身の外側の何も変更しない:ファイルも、グローバル変数も、ユーザーインターフェイスも(こんにちは、 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.NowRandom を使う関数は純粋ではない:

// 純粋ではない!
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 とコレクション

WhereSelect などは元のコレクションを触らずに新しいコレクションを返す。

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;
}
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION