Y.Hoshiをフォローする

【Javascript】高階関数の基礎とカリー化・部分適用

フロントエンド

まるで理系学部の一単元のようで偉そうなタイトルですが、安心してください。
当の筆者は地方大学の文系卒で、高校時代には数Ⅰで堂々の0点を獲得したこともあるくらい数学が苦手です。
つまり、当記事を読むにあたって、数学的な素養や複雑な理論への見識は必要ありません。
とりあえず、気軽に読み始めてください。

高階関数とは?

「高階関数」という言葉をご存じですか?
あまり聞きなじみがない方が多いかもしれませんが、Javascriptを普段書いている人であれば日常的にお世話になっているはずです。
使いこなせるといろいろと便利な概念なので、まずは高階関数の基礎を確認してみましょう。

「関数を引数に取る」か「関数を返す」関数

ずばり、高階関数とは「関数を引数や戻り値とする関数」のことを指します。
これは関数を引数に取るタイプの高階関数。

const fnc = (callback) => {
  callback();
}

これは関数から別の関数を返り値として返すタイプの高階関数。

const fnc = () => {
  return function() {
    console.log('hoge');
  }
}

Javascriptにおいては、関数は「第一級オブジェクト」として扱われるため、変数に格納したり引数や戻り値として指定することができます。

Javascriptで普段お世話になるような下記の関数(forEach, map, filter…)もみんな、高階関数です。

const ary = ['bird', 'animal', 'car'];
ary.forEach((item) => {
  console.log(item); // bird, animal, car
});

const mapped = ary.map((item) => {
  return item.toUpperCase(); // ['BIRD', 'ANIMAL', 'CAR']
});

const filtered = ary.filter((item) => {
  return item.indexOf('a') !== -1; // ['animal', 'car']
});

カリー化と部分適用

関数型言語を扱う方にとってはなじみ深い「カリー化」「部分適用」という用語🍛

手続型言語では意図的に使わなければあまりお世話にならないテクニックですが、使いかたを覚えておくとさまざまな場面で役立てることができます。

カリー化

カリー化」というのは複数の引数をとる関数を、1引数関数の連続した呼び出しに置き換えることを指します。

具体的にはこう。

function greet (name, age) {
  console.log('My name is ' + name + '. I am ' + age + ' years old.');
}
greet('taro', 27); // 引数を2つ渡している
// ↓↓↓
function greet (name) {
  return function (age) {
    console.log('My name is ' + name + '. I am ' + age + ' years old.');
  }
}
greet('taro')(27); // 引数の渡し方が変わっている!

関数の呼び出し方が変わりましたね🤔

greet関数をカリー化すると、「「ageを引数に取る新しい関数」を返す関数」になります。”(~~)”を2回連続してつなげているのは、引数を1つ適用して返ってきた関数に、さらに引数を渡しているからですね。

また、返す関数の中からさらに関数を返すこともできるので、こういう関数を作成することも可能です。

function deepNestedFnc (one) {
  return function (two) {
    return function (three) {
      return function (four) {
        return one + two + three + four;
      }
    }
  }
}
deepNestedFnc(1)(2)(3)(4); // 10

すこしモダンに、アロー記法で上記の関数を定義するとこんな感じになります。呼び出し方は同じ。

const deepNestedFnc = (one) => (two) => (three) => (four) => one + two + three + four;
deepNestedFnc(1)(2)(3)(4); // 10

このように、関数をカリー化すると、関数の呼び出しと引数適用のふるまいを変化させることができます
これが大事。

余談ですが、「カリー化」という名称は、純粋関数型言語Haskellの生みの親、「ハスケル・カリー(Haskell Curry)」に由来しているそう。

当のHaskellでは、あらゆる関数はすべてカリー化されています。
たとえば、下記一段目のような3値引数の関数があったとしたらそれは二段目のような関数定義と同じです。(“\x->”という記法はラムダ式を表しています)
add x y z = x + y + z
add = \x -> \y -> \z -> x + y + z

部分適用

では、カリー化できていると何がうれしいのか。

既にお伝えしているように、Javascriptでは関数は変数に格納することができるので、「関数に引数を途中まで適用した新しい関数」を定義することが可能になります。(これを「部分適用」といいます。)

const add = (a) => (b) => a + b;
const addTen = add(10); // 引数aに10を適用する
const addHundred = add(100); // 引数aに100を適用する
console.log(addTen(1)); // 11
console.log(addHundred(1)); // 101

上の例では10を追加する関数と100を追加する関数を同じ関数から生成することができました。

これだけだと何がうれしいかわからないかもしれないですが、「複数の処理の一部を共通化したい」という要件があった場合にはこの部分適用が役に立つことがあります。

次章で、具体的な例を見てみましょう。

高階関数の活躍する場面

引数を部分適用した新しい関数を作成する

Rubyは純粋オブジェクト指向言語でありながら、言語レベルでカリー化をサポートしています。

そんなわけで、まずはRubyでカリー化関数を用いた例を見てみましょう。

サッカーチームA,Bのメンバーをそれぞれ作成する処理を考えてみます。

## サッカーチームのメンバーを作成するクラス
## 初期化時に"チーム名"と"背番号"を指定する
class Member
  attr_accessor :team, :num
  def initialize team, num
    @team, @num = team, num
  end
  ## ...(詳細割愛)
end

# 引数を受け取りMemberインスタンスを作成する関数をカリー化する
to_member = ->team,num{ Member.new(team,num) }
to_member_curry = to_member.curry

# 第一引数にteamを部分適用した新しい関数を作成する
to_member_a = to_member_curry.('A')
to_member_b = to_member_curry.('B')

# 背番号を割り当てる
team_a = [*1..11].map(&to_member_a)
team_b = [*1..11].map(&to_member_b)

“to_member”は2値引数の関数なのでそのままmap()のコールバック関数として渡しても、期待している動作にはできません。

そこで、あらかじめ所属チームを示す’A’, ‘B’を関数にそれぞれ部分適用しておき、1値引数の別の関数として定義しなおします。

これにより、1つの関数をもとにして挙動の異なった2つの関数を作成することができ、map()のコールバックとすることでシンプルに処理を記述することができました。

また、副次的な効果として、引数を部分適用した関数を変数に格納することで、「どちらの関数を使用すればどちらのチームのメンバーインスタンスが作成できる」というのが明示的でわかりやすくなる、という利点もあります。

処理を抽象化・共通化する

アプリケーションを作成している中で、「複数の関数の同じ処理を共通化したい」という要件はいくらでも生まれてくるものです。

例えば、「APIのリクエストをサーバーサイドに送信する際、共通のエラーハンドリングやローディング表示をしたい」という場合。

const fetchList = async () => {
  showLoading();
  try {
    return await fetchApi();
  } catch (e) {
    handleError(e);
  }
  hideLoading();
}

const createRecord = async () => {
  showLoading();
  try {
    return await createApi();
  } catch (e) {
    handleError(e);
  }
  hideLoading();
}
...

各API処理毎に上記のtry ~ catchやローディング処理などを書くのが面倒くさい。そういう場合には、API処理を行う関数を引数に取る関数を作成するなどいかがでしょう。

const handleApi = async (api) => {
  showLoading();
  try {
    return await api();
  } catch (e) {
    handleError(e);
  }
  hideLoading();
}
const fetchList = async () =>  await handleApi(fetchApi);
const createRecord = async () => await handleApi(createApi);
const updateRecord = async () => await handleApi(updateApi);

すこしすっきりしました。

もしかしたら、APIの呼び出しによってエラーハンドリングを変えたい、という場合もあるかもしれません。

エラーハンドリング関数も引数に取ってみることにします。

const handleApi = async (api, handleErrorFnc) => {
  showLoading();
  try {
    return await api();
  } catch (e) {
    handleErrorFnc(e);
  }
  hideLoading();
}
const fetchList = async () => await handleApi(fetchApi, handleErrorFncA);
const createRecord = async () => await handleApi(createApi, handleErrorFncB);
const updateRecord = async () => await handleApi(updateApi, handleErrorFncB);

これで、API呼び出しの種類によって、エラーハンドリングを変更することができます。

でも、上記の書き方だと、エラーハンドリングが同じAPI呼び出しが複数ある場合でも、毎回エラーハンドリング関数をAPI実行時に引数に渡さなければなりません。 ちょっと面倒ですね🤔

こういう場面で、「カリー化」と「部分適用」が活躍します。

// ひとつめの関数にエラーハンドリング関数を渡せるようにカリー化
const handleApi = (handleErrorFnc) => async (api) => {
  showLoading();
  try {
    return await api();
  } catch (e) {
    handleErrorFnc(e);
  }
  hideLoading();
}
// handleApi関数に各エラー処理を部分適用
const handleApiCaseA = handleApi(handleErrorFncA);
const handleApiCaseB = handleApi(handleErrorFncB);

// 部分適用された関数に第二引数を渡して処理を実行する
const fetchList = async () => await handleApiCaseA(fetchApi);
const createRecord = async () => await handleApiCaseB(createApi);
const updateRecord = async () => await handleApiCaseB(updateApi);

エラーハンドリング処理を部分適用したhandleApiCaseA, handleApiCaseBという別々の関数を作成することができました。

あらかじめ別のファイルにモジュールとして定義しておけば、案件全体で使いまわすことができますね🎉

アプリケーション内で共通利用するエラーハンドリングをケースごとに部分適用しておけば、柔軟にエラーハンドリングができつつ、メンテナンス性の向上につながります。

関数合成

関数合成」というのは「複数の関数を組み合わせた新しい関数を作ること」です。

そのまんまですね。

関数合成などというといかにも高尚で難しい概念のように聞こえますが、実際は単純に「複数の関数に順番に引数を適用している」だけといえます。

よく目にするJavascriptでの関数合成の例がこんな感じです。

const compose = (...callbacks) => {
  return (arg) => {
    return callbacks.reduceRight((acc, callback) => {
      return callback(acc)
    }, arg)
  }
}

引数に渡したコールバック関数の配列(callbacks)にreduceRightメソッドを使うことで、各関数を右側から順番に適用していきます。

確かにこれで、「複数の関数を順番に適用する」という目的は達成することができますし、このコードは期待通りに関数を合成できます。

でも、これはどう見ても型安全じゃないです!

そんなわけで、型安全に関数合成を実現しているHakellの実装を参考にしてみます。

純粋関数型言語のHaskellには関数合成用の”.”(ドット)関数がありますが、型定義はこのようになっています。

(.) :: (b -> c) -> (a -> b) -> a -> c

これはつまり「「a型の値を引数にとってb型の値を返す関数」と「b型の値を引数にとってc型の値を返す関数」を組み合わせると「a型の値を引数にとってc型の値を返す関数」になる」と読めます(haskellの関数合成は右側から左側に向かって行われます)

この型定義をTypescriptで再現すると下記のような感じになります。 (Haskellの場合、関数はカリー化されているのでより正確にはこちらもカリー化するのが正しいですが、型推論がうまくいかなかったので2値引数の関数に書き直しています(悲涙))

const $ = (f: (b: B) => C, g: (a: A) => B): ((a: A) => C) => {
    return (a: A) => f(g(a));
}

この関数でHaskellの例と同様の関数合成を行ってみます。

“const composed = (a: number) => string;”と出ているように、正しく型推論がされていることが見てわかりますし、期待通りに動作しています🎉🎉🎉

また、最終的にstring型になった返り値を引数にadd10関数を呼ぶと?

これも、正しくエラーしてくれています!

これにて、関数の合成を型安全に行うことができました🍵

蛇足:高階関数コンポーネント(React)

高階関数の「処理を共通化する」という特性を生かした実装のひとつが、Reactでの「高階関数コンポーネント(HOC)」です。

HOCが活躍する場面は、「複数のコンポーネントで共通化したい横断的関心事」がある場合。

コンポーネントの初期化時などに共通して実行したいロジックなどを、関数コンポーネントの親コンポーネントに持たせることでロジックの再利用性を高めることができます。

ただ、最近のReactではReactHooksの台頭により、HOCが以前担ってきたような処理の共通化は必ずしも必要ではなくなってきています。とはいえ、こうした考え方を理解しておいた方がより柔軟なコンポーネント設計ができるようになるはずなので、理解しておいて損はないでしょう。 (蛇足&コードが長くなるのでサンプルは載せません。興味のある方は調べてみてください。)

まとめ

  • 高階関数は関数を引数に取ったり、新しい関数を返したりする関数
  • 複数の引数を持つ関数をカリー化すると、より汎用的に使える関数になるかも?

関数型言語の学習をするなかで、カリー化というおもしろいテクニックを学ぶことができたので、記事にしてみました。 不用意に使いまくる必要はないですが、要所要所でうまく使うことで、拡張性・保守性の高いコードを書けるようになりたいですね。