🍛

アロー関数が連続しているだと、、?!

2022/12/11に公開

はじめに

出会いは突然に、、
下記のようなコードに出会い、「なんじゃこりゃ、、?」となった時の備忘録になります。

const hogeHandler = (showNotification) => (message) => {
  // 何かしらの通知を表示する処理
  showNotification(message)
  
  // 省略
}

連続したアロー関数の正体

※アロー関数自体の説明については省略します。
連続したアロー関数の正体は、カリー化 というらしいです。
ふむ、、恥ずかしながら初耳でした。

// 何かしらの通知を表示する処理
showNotification(message)

この部分だけを見れば、なんとなく処理のイメージがつきますが、
問題は

const hogeHandler = (showNotification) => (message) =>

の部分です。
かなりモヤモヤしますね、、

このモヤモヤを晴らすべく、カリー化の正体について紐解いていきましょう。

前提知識

まず、カリー化の正体を暴く前に、いくつか前提知識を詰め込んでいきます。

第一級関数

MDNを見てみると、下記のように記載されています。

あるプログラミング言語が第一級関数 (First-class functions) を持つと言われる場合、その言語の関数がその他の変数と同様に扱われることを表します。例えば、こうした言語では、関数を他の関数への引数として渡したり、他の関数から返却したり、変数の値として代入したりすることができます。

https://developer.mozilla.org/ja/docs/Glossary/First-class_Function

大事な部分は、

その言語の関数がその他の変数と同様に扱われることを表します

の、「関数がその他の変数と同様に扱われる」という部分になります。
つまりざっくり表現すると、第一級関数 とは「関数を変数に代入できる」ことを表します。
JavaScriptやTypeScriptは第一級関数を持つ言語のため、
関数を変数に代入することができます。

const foo = () => { // 変数fooに関数を代入
  console.log("foobar");
}
foo(); // foobar

高階関数

同じくMDNを見てみると、下記のような記載がありました。

function sayHello() {
  return () => {
    console.log("Hello!");
  }
}

この例では、関数を他の関数から返す必要があります。 - 関数を返すことができるのは、 JavaScript では関数を値として扱っているからです。
メモ: 関数を返す関数は高階関数と呼ばれます。

https://developer.mozilla.org/ja/docs/Glossary/First-class_Function#関数を返す

上記の

関数を返すことができるのは、 JavaScript では関数を値として扱っているからです。

は、先ほど登場した「第一級関数」であることを表しています。
また、「関数を返す関数」のことを 高階関数と呼び、
上記の例だとsayHello は高階関数にあたります。

つまり、 JavaScriptは

  • 関数を値として扱うことができる(= 関数を変数に代入できる)
  • 関数を値として扱うことができるため、関数を返す関数(= 値を返す関数)を作ることができる

という言語であると表現することができます。

カリー化

ようやく本題です。
Wikipediaをみてみると、カリー化とは下記のように説明されています。

カリー化 (currying, カリー化された=curried) とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。

https://ja.wikipedia.org/wiki/カリー化

上記をざっくりとまとめると、カリー化とは「複数の引数を持つ関数を、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つの引数を受け取るsweetyspicy という関数に切り出したことで、
カツカレーと野菜カレーの「辛さ」の部分を共通化することができました。
言い方を変えると、「〇〇カレーに辛さだけを部分適用できた」ということができます。

上記コードのアロー関数を省略形にしてみると、

// 引数を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つ勉強になりました!

参考

https://developer.mozilla.org/ja/docs/Glossary/First-class_Function
https://cloudsmith.co.jp/blog/frontend/2021/09/1874653.html
https://zenn.dev/nekoniki/articles/5b7980fac91048775931
https://qiita.com/KDKTN/items/6a27c0e8efa66b1f7799
https://qiita.com/nouka/items/d9f29db7b6a69baa650a

Discussion