関数型プログラミング

参照透過性 さんしょうとうかせい

純粋関数副作用関数型プログラミングイミュータブルメモ化テスタビリティ
参照透過性について教えて

簡単に言うとこんな感じ!

「同じ入力を渡したら、いつでも必ず同じ答えが返ってくる」という性質だよ!たとえば add(2, 3) は何回呼んでも必ず 5 になるよね。これが参照透過性があるってこと!逆に「今日の日付を返す関数」は呼ぶたびに結果が変わるから参照透過性がない、ってことになるんだ。


参照透過性とは

参照透過性(Referential Transparency)とは、「ある式をその評価結果の値に置き換えても、プログラム全体の振る舞いが変わらない」という性質のことです。もっと平たく言えば、同じ引数を渡せば何度呼んでも必ず同じ値が返ってくるということです。数学の関数 f(x) = x + 1x3 なら常に 4 を返しますが、これが参照透過性の本質です。

参照透過性を持つ関数は純粋関数(Pure Function)とも呼ばれます。純粋関数は「外の世界(グローバル変数データベース・現在時刻・乱数など)に依存しない」「外の世界を変更しない(副作用がない)」という2つの条件を満たします。この性質があると、コードの動作を予測しやすくなり、テストやデバッグが格段に楽になります。

関数型プログラミングHaskell・Elm・Scalaなど)では参照透過性が中心的な概念として扱われていますが、JavaScriptPythonのような言語でも「参照透過性を意識した書き方」をすることで、バグが少なく保守しやすいコードを書けます。ビジネスシステム開発における品質向上の観点からも、近年注目が高まっている考え方です。


参照透過性がある/ない関数の違い

観点参照透過性あり(純粋関数)参照透過性なし(非純粋関数)
同じ入力への結果常に同じ変わる可能性がある
外部状態への依存なしあり(DB・時刻・グローバル変数など)
外部状態の変更(副作用)なしあり(ファイル書き込み・画面出力など)
テストのしやすさ非常に高いモックや準備が必要
実行順序の依存なしあり
典型例Math.abs(-3) → 常に 3Date.now() → 呼ぶたびに変わる
【純粋関数の例】               【非純粋関数の例】

function add(a, b) {          let count = 0;
  return a + b;               function increment() {
}                               count++;        ← 外部を変更!
// add(2, 3) は              }
// 何度呼んでも 5             // 呼ぶたびに count が変わる

覚え方:「数学の関数と同じかどうか?」

数学では f(x) = x²x = 3 なら絶対に 9 です。昨日も今日も明日も変わりません。「数学の関数と同じルールで動くか?」と問いかけてみると、参照透過性があるかどうかをすぐ判断できます。「引数以外のものを見ていないか?」「引数以外のものを変えていないか?」——この2点がチェックポイントです。

参照透過性を壊す代表的なパターン

  • グローバル変数の読み書き:関数の外にある変数を参照・変更する
  • 現在時刻・乱数:呼ぶたびに結果が変わるものに依存する
  • I/O操作:ファイル読み込み・DB検索・コンソール出力など
  • 例外のスロー:同じ入力でも例外が出る場合がある
  • ミュータブルなオブジェクトの変更:引数で受け取ったオブジェクトを直接書き換える

歴史と背景

  • 1930年代:数学者アロンゾ・チャーチがラムダ計算(λ計算)を考案。「関数を数学的に定義する」という考え方の原点になる
  • 1950年代:LISPが登場。関数を第一級市民として扱うプログラミング言語が生まれる
  • 1970〜80年代:Haskellの前身となるMLなどの関数型言語が研究され、「副作用のない純粋な関数」という概念が整理される
  • 1987年:Haskellの設計が始まる。参照透過性を強制的に守らせる言語設計として完成(1990年に正式リリース
  • 2000年代以降:並列処理・マルチコア時代の到来により、「副作用がなければ安全に並列実行できる」という参照透過性の実用的メリットが再評価される
  • 2010年代ReactのようなUIフレームワークが「状態管理を純粋関数で行う」設計(Reduxなど)を採用し、フロントエンド開発でも普及
  • 現在RustKotlinTypeScriptなどのモダンな言語・エコシステムでも「できるだけ副作用を局所化する」設計が主流になりつつある

純粋関数と副作用のある処理をどう共存させるか

現実のシステムはDBへの書き込みや画面への出力など、必ず「副作用」が必要です。では参照透過性はあきらめるしかないのでしょうか?実際の開発では「コアのロジックを純粋関数で書き、副作用は端に追い出す」という設計パターンが使われています。

「副作用を端に追い出す」設計パターン 入力層(副作用) DBから読み込む APIを叩く ファイルを読む 現在時刻を取得 ※テストしにくい モックが必要 コアロジック層(純粋) 計算・変換・判定 バリデーション ビジネスルール適用 データ加工・集計 ✓ テストが簡単 ✓ 再利用しやすい 出力層(副作用) DBに書き込む 画面に表示する メールを送る ログを出力する ※テストしにくい 統合テストで確認 データを渡す 結果を渡す 副作用は「端」に集め、テストしやすいコアを大きくする設計

この設計を「関数型コア、命令型シェル(Functional Core, Imperative Shell)」パターンと呼びます。コアのロジックを純粋関数で書くことで、そこだけを集中してユニットテストでき、品質を高めやすくなります。副作用のある部分(DBアクセスなど)は端に局所化し、統合テストで確認します。

参照透過性が実務で役立つ場面

場面参照透過性があると何が嬉しいか
ユニットテストモックや事前準備なしに、入力と出力だけでテスト可能
デバッグ「この関数を呼んだとき何が起きたか」が完全に再現できる
並列処理副作用がないので複数スレッドで安全に同時実行できる
メモ化(キャッシュ)同じ引数なら必ず同じ結果なので、計算結果を安全にキャッシュできる
リファクタリング式を値に置き換えても動作が変わらないので、安心してコードを整理できる

関連用語