JavaScriptの純粋関数(pure function)と副作用の違いを理解しよう
生徒
「先生、JavaScriptで『純粋関数』って何ですか?あと『副作用』って言葉もよく聞くのですが、どう違うんですか?」
先生
「いい質問ですね。純粋関数とは、決まった入力に対して必ず同じ結果を返し、プログラムの外に影響を与えない関数のことです。一方、副作用はその関数が外の世界に変化を与えることを指します。」
生徒
「もう少しわかりやすい例えとか、具体的な違いを教えてください!」
先生
「もちろんです。実際のコードも交えてゆっくり説明しますね。」
1. 純粋関数(pure function)とは?
純粋関数とは、次の2つのルールを守る関数のことです。
- 同じ入力には必ず同じ結果を返す
- 関数の外側の状態を変えたり、影響を与えたりしない
このルールを守ることで、関数の動きが予測しやすくなり、バグを減らしやすくなります。
例を見てみましょう。
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // 5
console.log(add(2, 3)); // 5(何度呼んでも結果は同じ)
このadd関数は純粋関数です。2と3を渡せば必ず5を返し、外の世界には影響を与えません。
2. 副作用(side effect)とは?
副作用とは、関数が外の世界に何か影響を与えることです。例えば、画面に文字を表示したり、ファイルを書き換えたり、変数の値を変更したりすることです。
副作用のある関数は同じ入力でも結果が変わったり、プログラムの状態を変えたりします。
例を見てみましょう。
let counter = 0;
function increment() {
counter = counter + 1; // 外の変数を変えているので副作用がある
console.log("カウンターは " + counter);
}
increment(); // カウンターは 1
increment(); // カウンターは 2
このincrement関数は副作用があります。関数を呼ぶたびに外のcounter変数の値が変わるからです。
3. 純粋関数と副作用の違いを簡単な例えで説明
純粋関数は「いつも同じ味のクッキーを作るレシピ」です。材料(入力)が同じなら必ず同じクッキー(結果)ができます。
副作用のある関数は「クッキーを作ると同時に、キッチンのテーブルも散らかすレシピ」です。クッキーは作れるけど、キッチンの状態も変わります。
プログラムでは、副作用があると予測が難しくなり、問題が起こりやすいです。
4. 純粋関数のメリット
- 結果が予測しやすい
- バグが少なく、テストもしやすい
- 関数を安全に再利用できる
プログラムをわかりやすく保つために、できるだけ純粋関数を使うことが推奨されます。
5. 副作用のある関数を使う場合の注意点
副作用は完全に悪いわけではありません。例えば、画面に表示したり、ファイルを保存したりするには副作用が必要です。
ただし、副作用を起こす関数はプログラムのどこで使われているかをよく理解し、管理することが大切です。
6. 純粋関数と副作用を意識したコード例
純粋関数で計算だけを行い、副作用は別の関数で行う例です。
// 純粋関数
function multiply(a, b) {
return a * b;
}
// 副作用のある関数
function showResult(result) {
console.log("計算結果は " + result);
}
// 使い方
const result = multiply(5, 3);
showResult(result);
計算は純粋関数で行い、画面表示は副作用のある関数で分けることで、プログラムが整理しやすくなります。
まとめ
今回の学習を通じて、JavaScriptにおけるプログラム設計の根幹とも言える「純粋関数(Pure Function)」と「副作用(Side Effect)」の本質的な違いについて深く掘り下げてきました。モダンなフロントエンド開発、特にReactやVue.jsといったフレームワークを活用する現場において、これらの概念を正確に理解しているかどうかは、コードの品質やメンテナンス性に直結する非常に重要なポイントとなります。
純粋関数がもたらす予測可能性の重要性
純粋関数とは、数学的な関数に近い存在です。入力値(引数)に対して出力値(戻り値)が一意に決まり、関数内部で完結している状態を指します。この「予測可能である」という性質は、大規模なアプリケーション開発において絶大な威力を発揮します。例えば、ある関数にバグが見つかった際、純粋関数であればその関数内部のロジックを確認するだけで済みますが、外部変数を参照している非純粋な関数の場合、どこで値が書き換わったのかをコード全体から探し出す「デバッグの迷宮」に迷い込むことになります。
副作用(サイドエフェクト)との上手な付き合い方
一方で、副作用は決して「悪」ではありません。私たちが普段利用しているWebアプリケーションで、画面に情報を表示したり、ボタンを押してデータをサーバーに保存したり、ブラウザのキャッシュを読み書きしたりする動作は、すべて副作用によって成り立っています。 大切なのは、「どこで副作用が発生しているかを明示的にし、管理下に置くこと」です。ロジックの計算部分を純粋関数に切り出し、データの出力や通信といった副作用を特定の場所に集約させることで、プログラムの堅牢性は飛躍的に向上します。
実践的なコード比較:TypeScriptでのアプローチ
より実務に近い形として、TypeScriptを用いた型安全な実装例を見てみましょう。データの変換処理(純粋)と、ログ出力(副作用)を分ける設計手法です。
/**
* 商品価格に消費税を加算する純粋関数
* @param price 税抜き価格
* @param taxRate 税率
* @returns 税込み価格
*/
const calculateTax = (price: number, taxRate: number): number => {
return Math.floor(price * (1 + taxRate));
};
/**
* 画面に計算結果を表示する副作用を持つ関数
*/
const displayPrice = (total: number): void => {
const message = `合計金額は ${total} 円です(税込)`;
console.log(message);
// DOM操作などもここで行うイメージ
};
// 実行
const finalPrice = calculateTax(1000, 0.1);
displayPrice(finalPrice);
上記のコードでは、calculateTaxは何度呼び出しても同じ数値を受け取れば同じ結果を返します。テストコードを書く際も、この関数に対して「1000と0.1を渡せば1100が返ってくること」を検証するだけで済み、非常にシンプルです。
エンジニアとしてのステップアップに向けて
JavaScript(ES6以降)では、map、filter、reduceといった配列メソッドが非破壊的、つまり元の配列を書き換えない「純粋」な思想で設計されています。これらを使いこなすことは、副作用を減らすための第一歩です。
まずは「自分の書いた関数が、外の変数に頼っていないか?」「予期せぬ場所で値を書き換えていないか?」を意識するだけで、あなたの書くコードは驚くほど読みやすく、修正しやすいものへと進化していくはずです。
生徒
「先生、まとめを読んで純粋関数と副作用の重要性がさらによく分かりました!副作用を完全にゼロにするのではなく、ちゃんと分けて管理するのがコツなんですね。」
先生
「その通りです。完璧に副作用をなくすと、何も表示されないアプリになってしまいますからね(笑)。大切なのは『計算』と『実行』を切り離す設計思想、いわゆる『関数型プログラミング』の考え方を取り入れることです。」
生徒
「TypeScriptの例を見て思ったのですが、純粋関数だとテストがしやすいっていうのは、外部の環境(ネットワークやグローバル変数)を気にしなくていいからですか?」
先生
「素晴らしい指摘です。純粋関数は『孤島』のようなものです。周囲の海の状態がどうであれ、島の中のルールは変わりません。だからこそ、特定の入力に対して期待通りの答えが返ってくるかを100%保証できるんです。これを『参照透過性』と呼びます。」
生徒
「参照透過性……かっこいい言葉ですね。これからコードを書くときは、まず『これは純粋関数にできるかな?』と自問自答してみます。例えば、現在の時刻を取得する関数はどうなりますか?」
先生
「おっと、鋭いですね。new Date()を使って現在時刻を取得する関数は、呼び出すたびに結果が変わるので『非純粋』な関数、つまり副作用ありと見なされます。そういった場合は、時刻を引数として受け取るように設計すれば、ロジック部分は純粋関数にできますよ。」
// これは非純粋(呼び出すタイミングで結果が変わる)
function getGreetingBad() {
const hour = new Date().getHours();
return hour < 12 ? "おはよう" : "こんにちは";
}
// これは純粋(引数に依存するのでテストしやすい)
function getGreetingGood(hour) {
return hour < 12 ? "おはよう" : "こんにちは";
}
console.log(getGreetingGood(10)); // 必ず「おはよう」
生徒
「なるほど!引数で渡すようにすれば、テストの時も『もし朝の10時だったら……』というシミュレーションが簡単にできますね。すごくスッキリしました!」
先生
「その調子です。この積み重ねが、将来的に保守性の高い大規模システムを支える技術になります。ぜひ日々のコーディングで意識してみてくださいね。」