関数型プログラミング
関数型プログラミング(Functional Programming)という言葉をよく耳にするようになりましたが、「難しそう」「数学の知識が必要?」といったイメージを持つ人も多いかもしれません。
この記事では、JavaScriptを使って関数型プログラミングの基本的な考え方と、実際の開発でどう役立つのかを解説します。
※2023年5月15日に内容を大幅に加筆・修正しました。
関数型プログラミングとは?
一言で言えば、**「プログラムを『計算(関数)』の組み合わせとして記述すること」**を目指すパラダイムです。
命令型プログラミングが「コンピュータへの命令の手順」を記述するのに対し、関数型プログラミングは「値がどう変換されるか」という**What(何をするか)**に焦点を当てます。
目的は、状態管理の複雑さと副作用を最小限に抑えることです。これにより、バグが少なく、予測可能で、テストしやすいコードを書くことができます。
命令型 vs 宣言型
違いを理解するために、具体的なコードを見てみましょう。「配列から偶数の数値だけを取り出し、それを2倍にする」という処理を考えます。
命令型のアプローチ
forループや一時変数を使い、「どのように処理するか(How)」を記述します。
const numbers = [1, 2, 3, 4, 5];
const doubledEvens = [];
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
doubledEvens.push(numbers[i] * 2);
}
}
console.log(doubledEvens); // [4, 8]
このコードは、CPUが何をするかを正確に記述していますが、「何がしたいのか」はコード全体を追わないと見えてきません。また、doubledEvens という状態を変化させています。
宣言型(関数型)のアプローチ
filter や map などを使い、「何がしたいか(What)」を記述します。
const numbers = [1, 2, 3, 4, 5];
const doubledEvens = numbers
.filter(n => n % 2 === 0) // 偶数だけに絞り込む
.map(n => n * 2); // 2倍にする
console.log(doubledEvens); // [4, 8]
手続きの詳細(ループカウンタの管理や配列へのpush)が隠蔽され、処理の意図が明確です。これが関数型プログラミングの入り口です。
主要な概念
関数型プログラミングを支える重要な3つの概念を紹介します。
1. 純粋関数 (Pure Functions)
純粋関数とは、以下の2つの条件を満たす関数です。
- 同じ入力に対して常に同じ出力を返す
- 副作用がない(外部の状態を変更したり、外部の状態に依存しない)
ダメな例(不純な関数):
let count = 0;
function increment() {
count++; // 外部変数に依存し、変更している(副作用)
return count;
}
良い例(純粋関数):
function increment(n) {
return n + 1; // 入力のみに依存し、常に同じ結果を返す
}
純粋関数は、実行コンテキストに依存しないため、テストが非常に簡単になります。
2. 不変性 (Immutability)
データは変更せず、新しいデータを生成します。
JavaScriptでは、const を使ってもオブジェクトのプロパティは変更できてしまいますが、関数型では意図的に変更を避けます。
/* 変更(Mutation) */
const user = { name: "Yuta", score: 10 };
user.score = 20; // 元のオブジェクトを変更
/* 不変(Immutability) */
const user = { name: "Yuta", score: 10 };
// スプレッド構文で新しいオブジェクトを作成
const updatedUser = { ...user, score: 20 };
データが不変であれば、「いつの間にかデータが変わっていた」というバグを防ぐことができます。
3. 高階関数 (Higher-Order Functions)
関数を「値」として扱います。関数を引数として受け取ったり、戻り値として関数を返したりできる関数のことです。
map や filter も、コールバック関数を受け取る高階関数の一種です。
JavaScriptでよく使う関数型メソッド
JavaScriptには強力な配列メソッドが標準で備わっています。
map: 変換する
配列の各要素に関数を適用し、新しい配列を作ります。
const prices = [100, 200, 300];
const taxIncluded = prices.map(price => price * 1.1);
// [110, 220, 330]
filter: 選別する
条件に合う要素だけを残した、新しい配列を作ります。
const users = [
{ name: "A", active: true },
{ name: "B", active: false },
{ name: "C", active: true }
];
const activeUsers = users.filter(user => user.active);
reduce: 集約する
配列を単一の値(数値、オブジェクト、別の配列など)にまとめます。最も汎用性が高いメソッドです。
const nums = [1, 2, 3, 4];
const sum = nums.reduce((acc, current) => acc + current, 0);
// 10
実践的なメリット
なぜ関数型プログラミングを学ぶ必要があるのでしょうか?
- バグが減る: 副作用を抑え、データ不変にすることで、予期せぬ状態変化によるバグが激減します。
- テストしやすい: 純粋関数はモックやセットアップが不要で、入力と出力だけを確認すればよいため、テストが容易です。
- コードが読みやすい: 宣言的に書くことで、コードが「何をしているか」を語るようになります。
まとめ
関数型プログラミングは、「全てを純粋関数にしなければならない」という厳しいルールではありません。
普段のコードの中で、
- なるべく副作用を減らす
- データを直接書き換えない
- 小さな純粋関数を組み合わせる
といった「関数型のエッセンス」を取り入れるだけでも、コードの品質は大きく向上します。まずは for ループを map や filter に置き換えるところから始めてみてください。