高階関数は、関数を引数に受け取ったり、関数を返したりできる関数のことです。
はじめは少し抽象的に聞こえますが、配列処理やイベント処理など、身近な場面で毎日のように使われています。
本記事では、定義→仕組み→メリット→使い方の順に、やさしい例と短いコードで丁寧に解説します。
高階関数とは?初心者の基本
高階関数の定義: 関数を受け取る/返す
高階関数とは「関数を引数として受け取る」または「関数を結果として返す」関数のことです。
ここでのコツは、関数も数字や文字列と同じように値として扱えると理解することです。
関数を受け取る例(JavaScript)
function applyTwice(fn, x) {
return fn(fn(x));
}
const plus1 = n => n + 1;
console.log(applyTwice(plus1, 5)); // 7
この例では、applyTwiceは関数plus1を引数fnとして受け取り、2回適用しています。
関数そのものを渡すのがポイントです。
関数を返す例(JavaScript)
function makeAdder(a) {
return function(b) {
return a + b;
};
}
const add10 = makeAdder(10);
console.log(add10(3)); // 13
makeAdderは新しい関数を返し、その関数はaの値を覚えています。
このように後から使える「振る舞い」を作って返すと考えると理解しやすいです。
高階関数のイメージ
イメージとしては、「レシピに調味料の差し替え余地がある」感じです。
レシピ(高階関数)は工程を用意し、塩やソースの部分(関数引数)は後から差し込めます。
あるいは「コンセント(高階関数)に、必要な家電(関数)を挿して使う」と考えてもよいでしょう。
共通の流れは高階関数に、細かい違いは渡す関数に任せるのがコツです。
よく使う場面
配列の繰り返し処理、並び替えの基準指定、イベントやタイマーのコールバック、ログやリトライなどの共通処理の注入など、実務で頻出します。
「処理の流れは決まっているが、細部の判断や具体的な振る舞いは差し替えたい」場面で特に威力を発揮します。
言語例
多くのモダン言語は高階関数をサポートします。
日常の配列操作用に標準で高階関数が用意されていることが多いです。
| 言語 | 代表的な高階関数 | 関数の書き方例 |
|---|---|---|
| JavaScript | map, filter, reduce, sort, forEach | x => x + 1 |
| Python | map, filter, sorted, reduce(funtools), any, all | lambda x: x + 1 |
| Ruby | map, select, reduce(inject), sort_by | ->(x) { x + 1 } |
| Kotlin | map, filter, fold, sortedBy | { x -> x + 1 } |
| Swift | map, filter, reduce, sorted(by:) | { x in x + 1 } |
| Java(8+) | stream.map, filter, reduce, sort | x -> x + 1 |
上のいずれの言語でも、関数やラムダを値として渡す仕組みが備わっています。
高階関数の仕組みを順番に理解
ステップ1: 関数は値として扱える
関数を変数に入れたり、配列に入れたり、引数として渡せるという基礎が出発点です。
コード例(JavaScript)
const add = (a, b) => a + b;
const op = add; // 関数を変数に入れる
console.log(op(2, 3)); // 5
関数を変数に代入しても、呼び出し方は同じです。
この自由さが高階関数の土台になります。
ステップ2: 高階関数に関数を渡す
関数を受け取る高階関数は、「流れは決めるが、要所の判断は関数に委ねる」スタイルを実現します。
コード例(JavaScript)
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, i => console.log("回数:", i));
repeatは回数の制御だけを担当し、実際の処理はactionに任せています。
渡すときにaction()と「呼び出さない」点に注意してください。
関数そのものを値として渡すのが正しい使い方です。
ステップ3: 高階関数が関数を返す
関数を返す高階関数は、「あとで呼び出せるカスタム振る舞い」を組み立てて返すと理解すると簡単です。
コード例(JavaScript)
function makeMultiplier(m) {
return function(x) {
return m * x;
};
}
const times3 = makeMultiplier(3);
console.log(times3(4)); // 12
返された関数は、作られたときのmの値を覚えています。
専門用語ではクロージャと呼びますが、ここでは「覚えているから後で使える」程度の理解で十分です。
ステップ4: 実行の流れ
実行の流れは、以下の順に進みます。
- 高階関数の定義がある(流れの骨格)。
- 小さな関数を用意する(差し替え部分)。
- 小さな関数を渡す、あるいは受け取った関数を返す。
- 高階関数が必要なタイミングでその関数を呼び出す。
ミニトレース(JavaScript)
function withLog(fn) {
return function(x) {
console.log("before");
const r = fn(x);
console.log("after");
return r;
};
}
const double = x => x * 2;
const loggedDouble = withLog(double); // 返された関数を受け取る
console.log(loggedDouble(5)); // before -> after -> 10
この例では、withLogが「共通の前後処理」を提供し、肝心の処理は引数fnに任せています。
役割がきれいに分かれることが分かります。
高階関数のメリット
重複が減り共通化できる
ループや前後の定型処理を高階関数にまとめると、同じコードを何度も書かずに済みます。
ログ、計測、リトライ、例外処理などを1か所に集約でき、仕様変更に強くなります。
修正は1か所で完了するので安全です。
コードが短く読みやすい
mapやfilterなどの高階関数を使うと、何をしたいのかが一目で分かります。
意図が名前に現れるため、読み手がループの細部を追う必要がなくなるからです。
実装の細部よりも、「何をしたいか」に集中できます。
再利用しやすく保守が楽
関数を受け取り返すことで、柔軟に組み合わせられる小さな部品を作れます。
要件が変わっても差し替える関数を変えるだけで対応しやすく、影響範囲を小さく保てます。
テストしやすい小さな単位に分けられる
高階関数が流れを、渡す関数が振る舞いを担当するので、それぞれを単体でテストしやすくなります。
テストが独立していれば、不具合の切り分けが簡単になります。
高階関数の使い方と例
配列処理の高階関数
配列処理ではmap, filter, reduceが代表的です。
「変換」「抽出」「集約」の3役を覚えると応用が効きます。
例(JavaScript)
const items = [1, 2, 3, 4, 5];
const doubled = items.map(x => x * 2); // [2, 4, 6, 8, 10]
const evens = items.filter(x => x % 2 === 0); // [2, 4]
const sum = items.reduce((acc, x) => acc + x, 0); // 15
mapは各要素を変換、filterは条件で選別、reduceは1つの値にまとめます。
要素ごとの処理は関数に任せ、反復は高階関数が担当します。
並び替えで使う高階関数
sortは並び順の決め方を関数で受け取ります。
並び替えの基準を差し替えられるのが利点です。
例(JavaScript)
const names = ["Tanaka", "suzuki", "Abe"];
// 大文字小文字を無視して並び替え
names.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
// ["Abe", "suzuki", "Tanaka"]
比較関数に書くことだけを変えれば、任意の基準に対応できます。
イベントやタイマーのコールバック
イベント処理や遅延実行では、「後で呼ぶ関数」を高階関数に渡します。
例(JavaScript)
button.addEventListener("click", () => {
console.log("clicked!");
});
setTimeout(() => {
console.log("1秒後に実行");
}, 1000);
addEventListenerやsetTimeoutは高階関数で、渡した関数を必要なタイミングで呼び出します。
条件や振る舞いを関数で差し替える
高階関数を使うと、方針だけを変えたいときに柔軟に対応できます。
例(JavaScript: 割引計算の戦略を差し替える)
function calcPrice(basePrice, discountFn) {
return discountFn(basePrice);
}
const noDiscount = p => p;
const tenPercent = p => Math.round(p * 0.9);
const flat300 = p => Math.max(0, p - 300);
console.log(calcPrice(1000, noDiscount)); // 1000
console.log(calcPrice(1000, tenPercent)); // 900
console.log(calcPrice(1000, flat300)); // 700
calcPriceは「計算の枠組み」を提供し、割引の中身は差し替え可能です。
条件分岐が増えても関数を追加するだけで拡張できます。
よくあるつまずき: 関数そのものを渡す
関数を「呼び出した結果」を渡してしまうケースが多いです。
例と説明
function run() { console.log("run"); }
setTimeout(run, 0); // 正: 関数そのものを渡す。後で実行される
setTimeout(run(), 0); // 誤: ここで実行されてしまい、渡るのは戻り値(undefined)
渡すのは「run」そのもの、呼ぶのは高階関数の中と覚えましょう。
よくあるつまずき: 短い関数をその場で書く
無名関数やアロー関数を使えば、小さな処理をその場で定義できます。
ただし、長くなりすぎる場合は別名の関数に切り出した方が読みやすいです。
例(JavaScript)
const result = [1,2,3].map(x => x * 2); // その場で短く書く
短いならインライン、長いなら命名して切り出すという基準が実務で役立ちます。
まとめ
高階関数は、関数を受け取るか返す関数で、配列処理やイベント処理など実務の多くの場面で活躍します。
仕組みはシンプルで、関数を値として扱えることさえ分かれば、渡す・返す・後で呼ぶという流れが自然に理解できます。
メリットは、重複の削減、読みやすさの向上、再利用性と保守性の改善、そしてテスト容易性です。
まずはmapやfilter、sortの比較関数、イベントのコールバックから慣れていき、「流れは高階関数、違いは渡す関数」という考え方を身につけていきましょう。
