2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

高階関数ってどんなときに使う?

Posted at

高階関数(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によって手を決めました。
hand1hand2play関数に渡してじゃんけんを行います。

ここで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関数が必要に応じてstrategyAstrategyBを呼び出せばよいので変更は容易です。
このように高階関数を使うと、じゃんけんの戦略のように複数の実装が考えられうる部分の変更を簡単にしてくれるのです。

私の場合は自分で高階関数を実装するほとんどの場合はこの利点を得るためです。

処理の管理や制御のための用途

例えば、JavaScriptでは呼び出し元の処理をロックしないようにIO処理を伴うような重い処理を行うライブラリでは多くの場合はコールバック関数を受け取る高階関数を提供しています。

const fs = require('fs');

fs.readFile("path/to/file.txt", 'utf-8', (err, data) => {
  // 読み込んだファイルを処理
})

// fs.readFileを待たずに以降が処理される。

JavaScriptの場合は特にことパターンが多いです(最近はPromiseが使われますが)。

また、コールバック関数は渡された高階関数側で実行や制御を制御できます。そのため、WebフレームワークのExpressやテストフレームワークのMochaなど多くのフレームワークでは利用者側はコールバック関数を渡す形式で使います。
こうすることでコールバック関数ではアプリケーションが着目すべき内容のみを記述し、面倒な副作用的な部分はフレームワークに任せることができます。

さらにコレクション系の処理ではよく出てきます。コレクション系は複数の値を扱うのですが、その部分の処理をmapfilterreduceなどに任せることで、使う側は1つの値に着目して処理を記述することができるのです。

[1, 2, 3].map(n => n * n)

mapでコレクションの制御は行ってくれているので、for分など使って制御しなくとも良いわけですね。

まとめ

高階関数は複数の実装の切り替えを容易にしてくれたり、コールバック関数の実行を管理したり制御するという使い方が便利だということがわかりました。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?