純粋関数

同じ入力に対して常に同じ出力を返し、副作用を持たない関数

関数型プログラミング設計

純粋関数とは

純粋関数 (Pure Function) は、(1) 同じ入力に対して常に同じ出力を返し、(2) 呼び出しても外部の状態を変えない関数である。この 2 条件が揃うと、関数呼び出しをその戻り値に置き換えてもプログラム全体の意味が変わらない。この性質を参照透過性と呼び、テストの容易さ、メモ化の安全性、並行実行での安全性はいずれもここから導かれる。

純粋 vs 不純

判定は 2 軸で行う。戻り値が引数だけで決まるか (入力側の暗黙依存がないか)、呼び出しの前後で外の世界が変わらないか (出力側の副作用がないか) である。次の 4 つを見比べると、崩れる軸が関数ごとに違うことが分かる。

// ✅ 純粋関数
function add(a: number, b: number): number {
  return a + b; // 入力のみに依存、外部状態を変更しない
}

function formatPrice(amount: number): string {
  return ${amount.toLocaleString()}`;
}

// ❌ 不純な関数
let total = 0;
function addToTotal(amount: number): number {
  total += amount; // 外部変数を変更 (副作用)
  return total;
}

function getCurrentTime(): string {
  return new Date().toISOString(); // 呼び出すたびに結果が変わる
}

純粋関数の条件

条件は出力側と入力側に分かれる。出力側は外の状態を書き換えないこと (グローバル変数の更新、DB 書き込み、ファイル出力、DOM 操作)。入力側は引数以外を読まないこと (グローバル変数、現在時刻、乱数、環境変数、DB の読み取り)。書き換えていなくても読んでいれば戻り値は変わりうるため、入力側だけが崩れた関数は「外を汚さないが純粋でもない」中間状態になる。前掲の getCurrentTime がその例だ。

テストの容易さ

純粋関数のテストは入力と出力の対応を書くだけで済み、前準備も後始末も要らない。不純な関数はグローバル状態の初期化と復元が必要で、後始末を 1 か所忘れるとテストの実行順序によって結果が変わる不安定なテストになる。

// ✅ 純粋関数: 入力と出力だけテストすればよい
test('add', () => {
  expect(add(1, 2)).toBe(3);
  expect(add(-1, 1)).toBe(0);
});

// ❌ 不純な関数: モック、セットアップ、クリーンアップが必要
test('addToTotal', () => {
  total = 0; // グローバル状態をリセット
  expect(addToTotal(10)).toBe(10);
  expect(addToTotal(20)).toBe(30);
  total = 0; // クリーンアップ
});

実践的なパターン

実務では計算を純粋関数へ切り出し、入出力 (DB・API・ファイル) を呼び出し側の端へ寄せる。純粋な部分は単体テストで条件を網羅し、端に残った薄い層だけをモックで確かめる形になる。

// ❌ ビジネスロジックと副作用が混在
async function processOrder(orderId: string) {
  const order = await db.get(orderId);       // 副作用
  const total = order.items.reduce((s, i) => s + i.price, 0);
  const tax = total * 0.1;
  await db.update(orderId, { total, tax });  // 副作用
}

// ✅ 純粋なロジックを分離
function calculateOrder(items: Item[]): { total: number; tax: number } {
  const total = items.reduce((s, i) => s + i.price, 0);
  return { total, tax: total * 0.1 };
}

async function processOrder(orderId: string) {
  const order = await db.get(orderId);
  const result = calculateOrder(order.items); // 純粋関数
  await db.update(orderId, result);
}

React と純粋関数

React はコンポーネントとフックが純粋であることをルールとして明文化している (React 公式ドキュメント、2026 年 8 月時点)。同じ props・state・context に対して常に同じ結果を返し、レンダー中に new Date()Math.random() を呼ばず、グローバル変数や DOM を直接操作せず、props・state を書き換えないことが要求される。props と state は書き換え可能なオブジェクトではなくその時点のスナップショットとして扱い、更新は新しい値を渡すか useState のセッターで行う。この前提があるため React はレンダーを中断・再実行しても結果が変わらないと仮定でき、副作用は useEffect などレンダー外へ隔離される。

配列操作の純粋性

JavaScript の配列メソッドは破壊的なものと非破壊的なものが混在しており、純粋性が崩れる典型的な場所になる。コピーを返す toSorted toReversed toSpliced with は ES2023 で追加され、Node.js 20 以降と 2023 年 7 月以降の主要ブラウザで使える。Node.js v26 で [3, 1, 2].toSorted()[1, 2, 3] を返し、元の配列は [3, 1, 2] のまま変わらないことを確認した。

// ❌ 破壊的メソッド (元の配列を変更)
arr.sort();    // 元の配列をソート
arr.push(1);   // 元の配列に追加

// ✅ 非破壊的メソッド (新しい配列を返す)
arr.toSorted();       // 新しいソート済み配列
[...arr, 1];          // 新しい配列
arr.filter(x => x > 0); // 新しい配列

見せかけの純粋関数

引数を書き換える関数は純粋に見えて純粋ではない。function addItem(cart, item) { cart.push(item); return cart; } は戻り値を返してはいるが、呼び出し側が渡した配列が変わるため参照透過性が崩れる。デバッグ用のログ出力、キャッシュへの書き込み、遅延初期化されるインスタンスの生成も同じく副作用だ。

メモ化 (計算結果のキャッシュ) が安全に効くのは純粋関数だけである。入力が同じでも結果が変わりうる関数をメモ化すると、古い値を返し続ける不具合になる。useMemoReact.memo を入れて挙動が壊れたときは、まず対象が本当に引数だけで決まっているかを疑うとよい。

この記事は役に立ちましたか?

関連用語

関連する記事