JavaScript開発において、純関数は欠かせない基礎知識です。これは、コードをシンプルにし、バグを減らすための関数型プログラミングの核となる概念です。
しかし、「純関数とは何か?」そして、純関数を扱うときに頻繁に出てくる用語「ミュータブル」「イミュータブル」「破壊処理」「非破壊処理」って何?と思う人も多いのではないでしょうか?
この記事では、これらの抽象的な概念を、具体的なコード実例を用いて解説しています。
純関数とは何か?
純関数(じゅんかんすう)とは、以下の2つの条件を完全に満たす関数のことです。
- 同じ入力には、必ず同じ出力(戻り値)を返す
関数の実行結果が、外部の状態(グローバル変数、時刻、ランダムな値など)に一切左右されず、渡される引数だけに依存する。 - 関数の外側に影響を与えない(副作用がない)
関数が自身のスコープ外にある外部の状態を一切変更しない。
現実世界で例えるなら「自動販売機」のようなものです。
金額とボタン(入力)が同じなら、出てくる飲み物(出力)は常に同じです。自動販売機が、誰かの財布の中身を勝手に変えたり(外部の状態の変更)、お釣りを出す以外に余計な動作(副作用)をすることはありません。
- 純関数は英語で、Pure Functionといいます。
- 副作用がある場合など、純関数以外を非純関数と呼びます。
【条件1】同じ入力には、必ず同じ出力(戻り値)を返すとは?
純関数は同じ入力の場合、常に同じ結果を返します。
例1
例えば以下のadd関数は、渡された引数が同じなら、出てくる結果も常に同じです。
function add(a, b) {
return a + b;
}
console.log(add(5, 3)); // 5,3を渡せば結果は常に8【条件2】関数の外側に影響を与えない(副作用がない)とは?
関数の外側に影響を与えない(副作用がない)ことは純関数の重要な条件です。
これは、関数が実行されても、その関数自身が受け取ったデータ以外、プログラム内の他の何も変わらないということです。
この「外側」には、グローバル変数、ファイル、データベース、画面などが含まれます。
非純関数:外側に影響を与える例(副作用あり)
例1:関数の外の変数の値を変える
副作用がある関数は、計算に加えて「何かをいじる」という行動を伴います。
つまり、関数が実行された結果、引数として渡されたデータの範囲を超えて、プログラムの外部の状態を変更してしまいます。
例えば、以下の場合、関数の外にあるグローバル変数を変更してしまいます。
let totalSales = 0;
function addToTotalSales(amount) {
totalSales = totalSales + amount;
}
addToTotalSales(500);
console.log(totalSales) //500 ※0から500に変わっている。例2:外部の画面に出力を行う
プログラムの外部であるファイルシステムや画面に出力する処理も副作用です。
console.log 、関数の外にある実行環境(ターミナルやブラウザの開発者ツールなど) に影響を与えるため、副作用を持つ操作と見なされます。
function logAndReturn(message) {
console.log("LOG: " + message);
return message;
}
logAndReturn("処理開始");
//関数の実行により、コンソール画面という外部環境に影響を与えた。純関数:外側に影響を与えない(副作用なし)
関数の外側にあるデータを変更したい場合、新しいオブジェクトや配列を作成して返します。
const user = { name: '山田', visit: 0 };
// スプレッド構文などを使って新しいオブジェクトを作成する
function incrementVisit(person) {
return { ...person, visit: person.visit + 1 };
}
const newUser = incrementVisit(user);
console.log(user); // { name: '山田', visit: 0 } ← 元のオブジェクトはそのまま
console.log(newUser); // { name: '山田', visit: 1 } ← 新しいオブジェクト純関数を作る際に、スプレッド構文をよく使います。スプレッド構文とは何かについては下記をご参考ください。
非純関数を純関数に書き換える例
実際に同じ結果を返す処理を、非純関数で書いた場合と、純関数で書いた場合のぞれぞれで紹介します。
使う関数は、入力値に税率をかけて税込み価格を返すものです。
非純関数:外部データに依存する
以下の関数は外部にあるグローバル変数 TAX_RATE に依存しています。
let TAX_RATE = 0.1;
function calculatePriceImpure(price) {
return price * (1 + TAX_RATE);
}
console.log(calculatePriceImpure(100)); // 110TAX_RATEが変わらなければ常に同じ値を返しますが、外部でTAX_RATEが書き換えられてしまうと、出てくる値が変わります。
let TAX_RATE = 0.1;
function calculatePriceImpure(price) {
return price * (1 + TAX_RATE);
}
TAX_RATE = 0.2 //※外部で書き換えられる
console.log(calculatePriceImpure(100)); // 120 ※同じ入力値で結果が変わった純関数:外部データに依存しない
関数の中で使うTAX_RATEも引数で渡されるようにすることで純関数にすることができます。
let TAX_RATE = 0.1;
// 渡された引数だけを使って計算する
function calculatePricePure(price, taxRate) {
return price * (1 + taxRate);
}
// 1回目の実行
console.log(calculatePricePure(100, 0.1)); // 110
TAX_RATE = 0.2;
// 2回目の実行
console.log(calculatePricePure(100, 0.1)); // 110※TAX_RATE が 0.08 に変わっていても、ここでは 0.1 を渡す限り結果は変わらない今回の例の場合、コードとしては非純関数の例の方がわかりやすいです。純関数=コードが簡略化されるということではありません。
それよりも、関数の外にあるデータを書き換えたことで、他のどこかでそのデータが呼び出されている場合に予期せぬバグを発生させないようにすることが目的です。
あくまで、純関数としての2つの条件を満たすということが大切です。
純関数を使うメリット
純関数を積極的に利用することで、コードは以下の点で改善されます。
挙動を予測しやすい(バグの減少)
常に同じ入力に対して同じ結果が保証されるため、関数の動作が非常に分かりやすく、予測しやすいです。
非純関数のように、外部の状態によって結果が変わる心配がないため、予期せぬバグが起きにくくなります。
テストが簡単にできる
外部依存や副作用がないため、テストに必要なのは「特定の入力(引数)を与えたときに、期待通りの出力(戻り値)が得られるか」を確認するだけです。
外部の状態(DB、ネットワーク、DOMなど)を準備したり(モック)、後始末したりする手間がいりません。
再利用しやすい
自己完結しているため、プログラムのどこにでも安全に持ち運び、再利用できます。
小さな純関数を組み合わせて、より複雑な処理を構築しやすいです。
キャッシュによるメモ化ができる
同じ入力に対して必ず同じ結果が返るため、一度計算した結果をキャッシュしておき、次回同じ入力が来たら計算をスキップしてキャッシュを返す「メモ化(Memoization)」という最適化手法が利用できます。
特にReactなどのライブラリでは、純粋性を利用して再レンダリングを最適化しています。
ミュータブルとイミュータブルとは何か?
純関数と「ミュータブル」「イミュータブル」という用語は非常に密接な関係を持ちます。
ミュータブルとイミュータブルは、データそのものが変更できるかどうかを表します。
また、「ミュータブルな操作/処理」「イミュータブルな操作/処理」というと、関数などの処理がもとのデータを変更しないという意味になります。
ミュータブル(Mutable)
ミュータブル(Mutable)は変更可能なデータです。作成した後で、元のデータを直接上書きできる性質を持ちます。
例えば、オブジェクトや配列は値を変えることができるのでミュータブルです。
const myArray = [1, 2, 3];
myArray.push(4);
console.log(myArray); // [1, 2, 3, 4] ← 元のデータが変更された!ミュータブル(Mutable)とは英語で「変更可能な」という意味です。派生語にミューテーション(Mutation)「突然変異」やミュータント(Mutant)「突然変異体」があります。
イミュータブル(Immutable)
イミュータブルは、ミュータブルにラテン語の否定を意味する「in-」をつけたものです。「変更できない・変化しない」という意味になります。
例えば、上記の配列もスプレッド構文を使うとイミュータブルな操作になります。
const myArray = [1, 2, 3];
let newArray = [...myArray, 4];
console.log(myArray); //[1, 2, 3] 元のデータはそのまま
console.log(newArray); //[1, 2, 3, 4] 新しいデータ破壊処理と非破壊処理とは何か?
ミュータブル・イミュータブルに密接した用語に「破壊処理」「非破壊処理」があります。
「破壊処理」と「非破壊処理」は、関数やメソッドが元のデータをどのように扱うかに焦点を当てた用語です。
簡単に言うと、以下のような関係性になります。
- 破壊処理 ≒ ミュータブルな処理
- 非破壊処理 ≒ イミュータブルな処理
破壊処理(Mutating Operation)
関数やメソッドが、引数として渡された(またはアクセスできる)元のデータを直接変更(上書き)する処理のことです。
ミュータブルなデータ型(配列、オブジェクト)に対して行われる操作で、破壊処理により副作用が発生し、非純関数になります。
非破壊処理(Non-Mutating Operation)
関数やメソッドが、元のデータを一切変更せず、変更を適用した新しいデータを作成して返すことです。
ミュータブルなデータ型に対しても、この非破壊的な手法を用いることで純関数を保つことができます。
純関数とミュータブル・イミュータブルの関係
純関数のルール(特に「副作用がない」)を守るには、ミュータブルなデータを扱う際にイミュータブルな操作(非破壊処理)をすることが決定的に重要になります。
NG例:破壊処理による副作用
ミュータブルなデータ(配列やオブジェクト)を関数内で直接変更すると、副作用が発生し、純関数ではなくなってしまいます。
const fruits = ['apple', 'banana']; // 外部にある配列(ミュータブル)
function addFruitImpure(arr, newFruit) {
arr.push(newFruit);
return arr;
}
const newFruits = addFruitImpure(fruits, 'orange');
console.log(fruits); // ['apple', 'banana', 'orange'] ※元のデータが意図せず変更OK例:非破壊処理による純関数
純関数を保つためには、ミュータブルなデータであっても、常に元のデータを変更せず、新しいコピーを作成して操作する必要があります。
const colors = ['red', 'blue']; // 外部にある配列(ミュータブル)
function addColorPure(arr, newColor) {
return [...arr, newColor];
}
const newColors = addColorPure(colors, 'green');
console.log(colors); // ['red', 'blue'] 元のデータは変更なし(非破壊処理=イミュータブルな操作)
console.log(newColors); // ['red', 'blue', 'green']
