アロー関数が連続しているだと、、?!
はじめに
出会いは突然に、、
下記のようなコードに出会い、「なんじゃこりゃ、、?」となった時の備忘録になります。
const hogeHandler = (showNotification) => (message) => {
// 何かしらの通知を表示する処理
showNotification(message)
// 省略
}
連続したアロー関数の正体
※アロー関数自体の説明については省略します。
連続したアロー関数の正体は、カリー化
というらしいです。
ふむ、、恥ずかしながら初耳でした。
// 何かしらの通知を表示する処理
showNotification(message)
この部分だけを見れば、なんとなく処理のイメージがつきますが、
問題は
const hogeHandler = (showNotification) => (message) =>
の部分です。
かなりモヤモヤしますね、、
このモヤモヤを晴らすべく、カリー化
の正体について紐解いていきましょう。
前提知識
まず、カリー化の正体を暴く前に、いくつか前提知識を詰め込んでいきます。
第一級関数
MDNを見てみると、下記のように記載されています。
あるプログラミング言語が第一級関数 (First-class functions) を持つと言われる場合、その言語の関数がその他の変数と同様に扱われることを表します。例えば、こうした言語では、関数を他の関数への引数として渡したり、他の関数から返却したり、変数の値として代入したりすることができます。
大事な部分は、
その言語の関数がその他の変数と同様に扱われることを表します
の、「関数がその他の変数と同様に扱われる」という部分になります。
つまりざっくり表現すると、第一級関数
とは「関数を変数に代入できる」ことを表します。
JavaScriptやTypeScriptは第一級関数を持つ言語のため、
関数を変数に代入することができます。
const foo = () => { // 変数fooに関数を代入
console.log("foobar");
}
foo(); // foobar
高階関数
同じくMDNを見てみると、下記のような記載がありました。
function sayHello() {
return () => {
console.log("Hello!");
}
}
この例では、関数を他の関数から返す必要があります。 - 関数を返すことができるのは、 JavaScript では関数を値として扱っているからです。
メモ: 関数を返す関数は高階関数と呼ばれます。
上記の
関数を返すことができるのは、 JavaScript では関数を値として扱っているからです。
は、先ほど登場した「第一級関数」であることを表しています。
また、「関数を返す関数」のことを 高階関数
と呼び、
上記の例だとsayHello
は高階関数にあたります。
つまり、 JavaScriptは
- 関数を値として扱うことができる(= 関数を変数に代入できる)
- 関数を値として扱うことができるため、関数を返す関数(= 値を返す関数)を作ることができる
という言語であると表現することができます。
カリー化
ようやく本題です。
Wikipediaをみてみると、カリー化とは下記のように説明されています。
カリー化 (currying, カリー化された=curried) とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。
上記をざっくりとまとめると、カリー化とは「複数の引数を持つ関数を、1つの引数を受け取る関数の連鎖にする」ことになります。
ここで、冒頭のコードを見てみると
const hogeHandler = (showNotification) => (message) => {
// 何かしらの通知を表示する処理
showNotification(message)
}
は、
① message
を引数にとる関数
② showNotification
を引数にとり、①を返す関数
のように、①と②が連鎖している関数と捉えることができます。
モヤモヤの正体
ただ、ここで疑問になるのが、
「そんな長ったらしい書き方する必要がないじゃないか!」という点です。
つまり、下記のような書き方をした方が、よりシンプルなように感じます。
const hogeHandler = (showNotification, message) => {
// 何かしらの通知を表示する処理
showNotification(message)
}
なぜカリー化するの?
カリー化のメリットはズバリ、引数を1つずつ処理していくことで、部分適用がしやすくなる
という点になります。
部分適用
とは、「関数の一部だけを適用する」ことであり、
逆にいうと「途中からは別の関数を使うことができる」というとことになります。
つまり、関数を実行する際に、共通化した処理を部分適用
していくことで、
その関数をより汎用的に使えるようになるということができます。
下記のコードを例に考えてみます。
const menu = (hotFlavor, dish) => {
return hotFlavor + dish + "カレー"
}
menu("辛口", "カツ") // 辛口カツカレー
引数を2つ受け取り、〇〇カレーを作る関数です。
このままだと種類が少なく寂しいので、
メニューを追加します。
const menu = (hotFlavor, dish) => {
return hotFlavor + dish + "カレー"
}
menu("辛口", "カツ") // 辛口カツカレー
menu("甘口", "カツ") // 甘口カツカレー
menu("辛口", "野菜") // 辛口野菜カレー
だいぶメニューが増えました。
現状〇〇カレーを作る関数に2つの引数を渡していますが、
「辛口」の部分は複数のメニューで重複しており、少し冗長に感じます。
そこで、部分適用
を使って甘口
と辛口
かだけを決める関数を作成していきます。
const menu = (hotFlavor) => {
return (dish) => {
return hotFlavor + dish + "カレー"
}
}
// 辛さを部分適用した関数
const spicy = (dish) => menu("辛口")
const sweety = (dish) => menu("甘口")
spicy()("カツ") // 辛口カツカレー
sweety()("カツ") // 甘口カツカレー
spicy()("野菜") // 辛口野菜カレー
2つの引数を受け取るsweety
と spicy
という関数に切り出したことで、
カツカレーと野菜カレーの「辛さ」の部分を共通化することができました。
言い方を変えると、「〇〇カレーに辛さだけを部分適用できた」ということができます。
上記コードのアロー関数を省略形にしてみると、
// 引数を1つずつ受け取るようカリー化
const menu = (hotFlavor) => (dish) => hotFlavor + dish + "カレー"
// 辛さを部分適用した関数
const spicy = (dish) => menu("辛口")
const sweety = (dish) => menu("甘口")
spicy()("カツ") // 辛口カツカレー
sweety()("カツ") // 甘口カツカレー
spicy()("野菜") // 辛口野菜カレー
のようになります。
ここで、
// 引数を1つずつ受け取るようカリー化
const menu = (hotFlavor) => (dish) =>
に着目してみると、
冒頭の
const hogeHandler = (showNotification) => (message) =>
と同じ形式になっていることがわかります。
つまり、冒頭のコードを
const hogeHandler = (showNotification, message) => {
// 何かしらの通知を表示する処理
showNotification(message)
}
のようにしていなかった理由は、
showNotification
の部分を共通化して部分適用
するために、
カリー化して受け取りやすい形にしていたためでした。
(実際、showNotificationには通常の通知処理、エラー通知の処理などが共通化されていました)
まとめ
連続したアロー関数の正体はカリー化
と呼ばれるものでした。
部分適用
と カリー化
についてまとめると、下記の通りです。
- 部分適用: 関数の一部だけを適用する → 処理の共通化をすることができる
- カリー化: 引数を一つずつ受け取る連鎖にする → 部分適用しやすくなる
今回のように通知を行う際の通知種類だけが違う場合や、
API呼び出しのメソッド・エラーハンドリングだけが違う場合などで
カリー化、および部分適用を用いることができそうです。
また1つ勉強になりました!
参考
Discussion