高階関数(higher-order function)が流行っているので、高階関数をどんなときに使うのかを書いてみます。
高階関数というと「関数を関数に渡す」ということをイメージできますが、実際にどういう風に使うものかと聞かれると「関数とかサブルーチン」を渡したいときに使うものだよくらいにしか答えられないのでよくよく考えてみました。
私はよく使うと思うのは次の用途が多いと思います。
- 実装の切り替えを柔軟にする用途 (関数を受け取る関数)
- 処理の管理や制御のための用途 (関数を受け取る関数)
- (省略)関数の構築を簡単にする用途 (関数を返す関数)
特に「関数を返す関数」も高階関数と呼ぶようですが、今回はややこしくなるので高階関数のうちでも「関数を受け取る関数」についてみていきたいと思います。
実装の切り替えを柔軟にする用途
高階関数を使わないと
もし仮に言語に関数に関数を渡すことができなかったとしましょう。
(大抵の言語は関数を関数に渡せるかそれに準ずる機能を提供してます。)
じゃんけんをするプログラムを書いた場合は以下のようになるでしょう。
// じゃんけんする関数
const play = (handA, handB) => {
// TODO 勝敗の判定処理
console.log(`handAの勝ち or 引き分け or 負け`)
}
const randomStrategy = () => { /* TODO ランダムで手を出す */ },
const unixTimeModStrategy = () => { /* TODO UNIX時間を3で割った余りによって手を出す */ }
const handA = randomStrategy()
const handB = dayModStrategy()
play(handA, handB)
hand1
はランダムに手を出すというrandomStrategy
によって手を決め、hadn2
はUNIX時間を3で割った余りによって手を出すというunixTimeModStrategy
によって手を決めました。
hand1
とhand2
をplay
関数に渡してじゃんけんを行います。
ここで3回勝負に書き換えます。三回勝負するのでplayで手をそれぞれ配列でもらうことにしましょう。
// じゃんけんする関数
const play = (handsA, handsB) => { // 配列で受け取る
// TODO 勝敗の判定処理
console.log(`handsAの勝ち or 引き分け or 負け`)
}
const randomStrategy = () => { /* TODO ランダムで手を出す */ },
const unixTimeModStrategy = () => { /* TODO UNIX時間を3で割った余りによって手を出す */ }
// 3回分の手をあらかじめ作る
const handsA = [randomStrategy(), randomStrategy(), randomStrategy()]
const handsB = [dayModStrategy(), dayModStrategy(), dayModStrategy()]
play(handsA, handsB)
少し怪しいコードになってきました。さらにrandomStrategy
を別の戦略に変えます。
// じゃんけんする関数
const play = (handsA, handsB) => {
// TODO 勝敗の判定処理
console.log(`handsAの勝ち or 引き分け or 負け`)
}
const newStrategy = () => { /* TODO 新しい別の戦略で手を出す */ },
const unixTimeModStrategy = () => { /* TODO UNIX時間を3で割った余りによって手を出す */ }
const handsA = [newStrategy(), newStrategy(), newStrategy()] // 新しい戦略で手を生成
const handsB = [dayModStrategy(), dayModStrategy(), dayModStrategy()]
play(handsA, handsB)
ここで戦略を変更しましたが、書き換える箇所が多く改修コストが高いことに気づきます。
さらに三本先取の場合に変更したくなった場合はどうするでしょうか?play
関数もplay
関数の呼び出し側も書き換えなくてはいけません。じゃんけんプログラムの変更されうる可能性について柔軟性が低いと言えます。
高階関数を使うと
では高階関数を使うとどのように書けるでしょうか?
// じゃんけんする関数
const play = (strategyA, strategyB) => { // 関数を渡す
const handA = strategyA()
const handB = strategyB()
// TODO 勝敗の判定処理
console.log(`strategyAの勝ち or 引き分け or 負け`)
}
const randomStrategy = () => { /* TODO ランダムで手を出す */ },
const dayModStrategy () => { /* TODO 日数を3で割った余りによって手を出す */ }
play(randomStrategy, dayModStrategy)
これを三本勝負に書き換えます。
// じゃんけんする関数
const play = (strategyA, strategyB) => {
const handA1 = strategyA()
const handB1 = strategyB()
const handA2 = strategyA()
const handB2 = strategyB()
const handA3 = strategyA()
const handB3 = strategyB()
// TODO 勝敗の判定処理
console.log(`strategyAの勝ち or 引き分け or 負け`)
}
const randomStrategy = () => { /* TODO ランダムで手を出す */ },
const dayModStrategy () => { /* TODO 日数を3で割った余りによって手を出す */ }
play(randomStrategy, dayModStrategy)
play
の中を三回勝負に変更すればよく、play
の呼び出し元は特に変更する必要はありません。
次に戦略を書き換えてみます。
// じゃんけんする関数
const play = (strategyA, strategyB) => {
const handA1 = strategyA()
const handB1 = strategyB()
const handA2 = strategyA()
const handB2 = strategyB()
const handA3 = strategyA()
const handB3 = strategyB()
// TODO 勝敗の判定処理
console.log(`strategyAの勝ち or 引き分け or 負け`)
}
const newStrategy = () => { /* TODO 新しい別の戦略で手を出す */ },
const dayModStrategy () => { /* TODO 日数を3で割った余りによって手を出す */ }
play(newStrategy, dayModStrategy)
変更箇所は非常に少なくて済みました。仮に三本先取に書き換えるとしても呼び出し元の変更の必要はなく、play
関数が必要に応じてstrategyA
、strategyB
を呼び出せばよいので変更は容易です。
このように高階関数を使うと、じゃんけんの戦略のように複数の実装が考えられうる部分の変更を簡単にしてくれるのです。
私の場合は自分で高階関数を実装するほとんどの場合はこの利点を得るためです。
処理の管理や制御のための用途
例えば、JavaScriptでは呼び出し元の処理をロックしないようにIO処理を伴うような重い処理を行うライブラリでは多くの場合はコールバック関数を受け取る高階関数を提供しています。
const fs = require('fs');
fs.readFile("path/to/file.txt", 'utf-8', (err, data) => {
// 読み込んだファイルを処理
})
// fs.readFileを待たずに以降が処理される。
JavaScriptの場合は特にことパターンが多いです(最近はPromise
が使われますが)。
また、コールバック関数は渡された高階関数側で実行や制御を制御できます。そのため、WebフレームワークのExpressやテストフレームワークのMochaなど多くのフレームワークでは利用者側はコールバック関数を渡す形式で使います。
こうすることでコールバック関数ではアプリケーションが着目すべき内容のみを記述し、面倒な副作用的な部分はフレームワークに任せることができます。
さらにコレクション系の処理ではよく出てきます。コレクション系は複数の値を扱うのですが、その部分の処理をmap
やfilter
、reduce
などに任せることで、使う側は1つの値に着目して処理を記述することができるのです。
[1, 2, 3].map(n => n * n)
map
でコレクションの制御は行ってくれているので、for
分など使って制御しなくとも良いわけですね。
まとめ
高階関数は複数の実装の切り替えを容易にしてくれたり、コールバック関数の実行を管理したり制御するという使い方が便利だということがわかりました。