読み込み中...
読み込み中...
読み込み中...
読み込み中...
読み込み中...
**純粋関数(Pure Function)**は、関数型プログラミングの最も基本的な概念です。純粋関数は以下の2つの条件を満たします。
**副作用(Side Effect)**とは、関数の外部の状態を変更する操作のことです。
| 副作用の例 | 説明 |
|---|---|
| DOM の操作 | 画面の表示を変更する |
| HTTP リクエスト | サーバーにデータを送信する |
console.log | コンソールに出力する |
| 変数の書き換え | 外部スコープの変数を変更する |
| ファイルの読み書き | ファイルシステムにアクセスする |
純粋関数は予測可能でテストしやすく、並列処理にも安全です。ただし、現実のアプリケーションには副作用が不可欠です。重要なのは、純粋な部分と副作用を持つ部分を明確に分離することです。
純粋関数と不純な関数の比較
// 純粋関数の例
function add(a, b) {
return a + b; // 同じ入力なら常に同じ出力
}
function formatPrice(price) {
return `¥${price.toLocaleString()}`;
}
// 不純な関数の例
let total = 0;
function addToTotal(amount) {
total += amount; // 副作用: 外部変数を変更
return total;
}
console.log(addToTotal(100)); // 100
console.log(addToTotal(100)); // 200(同じ入力なのに異なる出力)
// 純粋に書き直す
function addToTotalPure(currentTotal, amount) {
return currentTotal + amount; // 副作用なし
}
console.log(addToTotalPure(0, 100)); // 100
console.log(addToTotalPure(0, 100)); // 100(同じ入力 → 同じ出力)
console.log(addToTotalPure(100, 100)); // 200チャレンジ
不純な関数 getDiscountedPrice を純粋関数に書き直してください。グローバル変数 discountRate に依存せず、引数で割引率を受け取るようにしてください。
ポイント
純粋関数は同じ入力に対して常に同じ出力を返し、副作用がありません。テストしやすく予測可能なコードを書くために、純粋な部分と副作用を持つ部分を明確に分離することが重要です。
**高階関数(Higher-Order Function)**は、関数を引数として受け取るか、関数を返す関数です。JavaScriptでは関数は第一級オブジェクトであるため、変数に代入でき、引数や戻り値として自由に扱えます。
代表的な高階関数は配列のメソッドです。
| メソッド | 動作 | 戻り値 |
|---|---|---|
map(fn) | 各要素を変換 | 新しい配列 |
filter(fn) | 条件に合う要素を抽出 | 新しい配列 |
reduce(fn, init) | 要素を1つの値に集約 | 累積値 |
find(fn) | 条件に合う最初の要素 | 要素 or undefined |
every(fn) | 全要素が条件を満たすか | boolean |
some(fn) | 1つでも条件を満たすか | boolean |
**関数合成(Function Composition)**は、複数の関数を組み合わせて新しい関数を作る手法です。compose(右から左)と pipe(左から右)が代表的なパターンです。
配列メソッドのチェーン
// map / filter / reduce のチェーン
const products = [
{ name: "りんご", price: 150, inStock: true },
{ name: "バナナ", price: 100, inStock: true },
{ name: "ぶどう", price: 300, inStock: false },
{ name: "みかん", price: 80, inStock: true }
];
// 在庫ありの商品の合計金額
const totalInStock = products
.filter(p => p.inStock) // 在庫あり
.map(p => p.price) // 価格を抽出
.reduce((sum, price) => sum + price, 0); // 合計
console.log(totalInStock); // 330pipe による関数合成
// pipe 関数: 左から右に関数を合成
function pipe(...fns) {
return function(value) {
return fns.reduce((acc, fn) => fn(acc), value);
};
}
// 小さな純粋関数を定義
const trim = str => str.trim();
const toLower = str => str.toLowerCase();
const addPrefix = str => "user_" + str;
// pipe で合成
const normalizeUsername = pipe(trim, toLower, addPrefix);
console.log(normalizeUsername(" Tanaka ")); // "user_tanaka"
console.log(normalizeUsername(" SATO ")); // "user_sato"チャレンジ
pipe 関数を実装してください。複数の関数を引数として受け取り、左から右の順に適用する新しい関数を返します。
ポイント
高階関数は関数を引数に取るか関数を返す関数です。map/filter/reduce は代表的な高階関数で、データ変換をチェーンできます。pipe による関数合成は、小さな純粋関数を組み合わせて複雑な処理を構築する手法です。
**カリー化(Currying)**とは、複数の引数を取る関数を、1つの引数を取る関数の連鎖に変換する手法です。f(a, b, c) を f(a)(b)(c) の形に変換します。
**部分適用(Partial Application)**は、関数の引数の一部を事前に固定して新しい関数を作る手法です。bind を使った部分適用は前章で学びましたが、カリー化はより体系的なアプローチです。
| 概念 | 説明 | 例 |
|---|---|---|
| カリー化 | f(a, b) → f(a)(b) に変換 | curry(add)(1)(2) |
| 部分適用 | 引数の一部を固定 | add.bind(null, 1) |
カリー化の利点は、関数の再利用性が高まることです。共通のパラメータを事前に適用した特化関数を簡単に作れます。
カリー化の基本と汎用 curry 関数
// 手動カリー化
function multiply(a) {
return function(b) {
return a * b;
};
}
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// 汎用 curry 関数
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
};
}
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
console.log(curriedAdd(1, 2, 3)); // 6カリー化は設定値を事前に固定した関数を作る場面で威力を発揮します。ログ関数やバリデーション関数、データフォーマット関数などで活用されます。
カリー化の実用パターン
// 実用例: ログ関数
const log = (level) => (module) => (message) =>
console.log(`[${level}] [${module}] ${message}`);
const errorLog = log("ERROR");
const authError = errorLog("AUTH");
authError("ログインに失敗しました");
// "[ERROR] [AUTH] ログインに失敗しました"
// 実用例: フィルタ関数
const filterBy = (key) => (value) => (arr) =>
arr.filter(item => item[key] === value);
const filterByStatus = filterBy("status");
const getActive = filterByStatus("active");
const users = [
{ name: "田中", status: "active" },
{ name: "佐藤", status: "inactive" },
{ name: "鈴木", status: "active" }
];
console.log(getActive(users));
// [{ name: "田中", status: "active" }, { name: "鈴木", status: "active" }]チャレンジ
カリー化された関数 createFormatter を実装してください。currency(通貨記号)を受け取り、decimals(小数桁数)を受け取り、最後に amount(金額)を受け取って "¥1,000" のようなフォーマット済み文字列を返します。
ポイント
カリー化は f(a, b) を f(a)(b) に変換し、部分適用は引数の一部を事前に固定します。設定値の事前適用やフィルタ関数の生成など、関数の再利用性を高める強力な手法です。
**イミュータビリティ(不変性)**とは、一度作成したデータを変更しないという原則です。データを変更する代わりに、変更を反映した新しいデータを作成します。
| 概念 | 説明 |
|---|---|
| ミュータブル(可変) | 作成後にデータを変更できる |
| イミュータブル(不変) | 作成後にデータを変更しない。変更時は新しいコピーを作る |
イミュータビリティの利点は以下の通りです。
===)だけで変更を検出できる(React 等で重要)JavaScript のプリミティブ値(数値、文字列、boolean等)は本質的にイミュータブルですが、オブジェクトと配列はミュータブルです。イミュータブルに扱うためのテクニックを学びましょう。
スプレッド構文によるイミュータブルな更新
// ミュータブルな操作(元のデータを変更してしまう)
const original = { name: "田中", scores: [80, 90] };
const copy = original;
copy.name = "佐藤"; // 元のデータも変更される!
console.log(original.name); // "佐藤"
// イミュータブルな操作(スプレッド構文で新しいオブジェクトを作成)
const user = { name: "田中", age: 30, address: { city: "東京" } };
// シャローコピー(1階層目のみコピー)
const updated = { ...user, age: 31 };
console.log(user.age); // 30(元のデータは変更されない)
console.log(updated.age); // 31
// ネストされたオブジェクトのイミュータブル更新
const movedUser = {
...user,
address: { ...user.address, city: "大阪" }
};
console.log(user.address.city); // "東京"
console.log(movedUser.address.city); // "大阪"配列のイミュータブル操作と Object.freeze
// 配列のイミュータブルな操作
const nums = [1, 2, 3, 4, 5];
// 追加: push ではなく concat またはスプレッド
const added = [...nums, 6];
// 削除: splice ではなく filter
const removed = nums.filter(n => n !== 3);
// 更新: 直接代入ではなく map
const doubled = nums.map(n => n * 2);
console.log(nums); // [1, 2, 3, 4, 5](元の配列は変更されない)
console.log(added); // [1, 2, 3, 4, 5, 6]
console.log(removed); // [1, 2, 4, 5]
console.log(doubled); // [2, 4, 6, 8, 10]
// Object.freeze で浅い凍結
const config = Object.freeze({
apiUrl: "https://api.example.com",
timeout: 5000
});
// config.timeout = 10000; // strict mode では TypeErrorチャレンジ
関数 updateUser を実装してください。user オブジェクトと updates オブジェクトを受け取り、元の user を変更せずに、updates を反映した新しいオブジェクトを返してください。
ポイント
イミュータビリティは「データを変更せず、新しいコピーを作る」原則です。スプレッド構文で浅いコピーを作成し、Object.freeze で凍結できます。配列は map/filter/concat でイミュータブルに操作しましょう。
実務での活用
「同じ入力に対して常に同じ結果を返す」純粋関数を意識するだけで、テストのしやすさとバグの少なさが劇的に向上します。配列操作で push(破壊的)ではなくスプレッド構文(非破壊的)を選ぶ習慣は、データの予期しない変更を防ぐ実践的なテクニックです。
用語