🐝

【TS】今さら聞けない高階関数・カリー化

2020/11/02に公開3

はじめに

普段はReactReactNativeを使ってWebやネイティブアプリ開発を行っています。
これらを扱っていくにあたり、関数型プログラミングの考え方に触れることが多く、ひいては「高階関数」や「カリー化」といった用語の理解が必要となります。

人に教えていくと、このあたりのイメージが掴みにくいようなので、いったんソースを追う形で整理していきたいと思います。
なお、この記事ではReactReactNativeを利用しない方のために「高階関数」と「カリー化」の説明に留めています。
「高階コンポーネント」についてはいずれ別記事にて解説していく予定です。

第一級関数

高階関数を理解するにあたって「第一級関数」という用語を知っておく必要があります。
第一級関数のWikipediaには以下のような説明があります。

関数を第一級オブジェクトとして扱うことのできるプログラミング言語の性質、またはそのような関数

第一級オブジェクトとは「生成・代入・演算・(引数・戻り値としての)受け渡し」といった基本操作を制限なしに使用できるオブジェクトを指します。
ここでポイントなのは「(引数・戻り値としての)受け渡し」です。

つまり「第一級関数」は「関数」を「引数や戻り値」として扱うことができる関数、と読み解くことができます。

高階関数

高階関数のWikipediaには以下のような説明があります。

高階関数(こうかいかんすう、英: higher-order function)とは、第一級関数をサポートしているプログラミング言語において少なくとも以下のうち1つを満たす関数である。

  • 関数(手続き)を引数に取る
  • 関数を返す

つまり「高階関数」とは「関数」を「引数もしくは戻り値、あるいは両方」として扱う関数、と読み解けます。

関数を引数として扱うケースは、JSTSの世界ではコールバック関数が有名どころです。

const arr: number[] = [1,2,3];

// forEach()は「nを引数に取る関数」を引数として受けているため高階関数である
arr.forEach((n: number) => {
  console.log(n);
});

反対に関数を戻り値として扱うケースは以下です。

// counterFactory()は「cntをインクリメントしコンソール表示する関数」を戻り値とするため高階関数である
const counterFactory = (): () => void => {
  let cnt: number = 0;
  return () => console.log(++cnt);
}

// counterFactory()を実行して関数を生成
const counter1: () => void = counterFactory();
const counter2: () => void = counterFactory();

counter1(); // --> 1
counter1(); // --> 2
counter1(); // --> 3

counter2(); // --> 1
counter2(); // --> 2

カリー化

続いてカリー化です。
カリー化は上記の高階関数の特性を利用したものになります。
カリー化のWikipediaでは以下のように説明されています。

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

要するに「複数の引数を関数」を分割するというものです。
これの何が便利かというと「引数の一部だけを固定して、関数を複製する」ことができます。これを 「部分適用」 と呼びます。

例えば以下のように、「リクエスト先URL」と「メソッド」と「パラメータ」という3種類の引数から非同期処理を行うfetch()という汎用関数を作ったとします。

const fetch = (url : string, method: 'POST' | 'GET', param : {}) => {
    // 3種類の引数を使って非同期処理を実行...
}

このfetchを使う場合は、以下のように呼び出すはずです。

// リクエスト①
fetch('http://hogehogehogehoge', 'POST', { value: 'xxxx'});
// リクエスト②
fetch('http://fugafugafugafuga', 'POST', { value: 'xxxx'});
// リクエスト③
fetch('http://fugafugafugafuga', 'GET', {});

この場合、リクエストの①と②はURLが違うだけです。また、②と③はメソッドとパラメータが違うだけです。
一部のパラメータが違うだけなのに、その他のパラメータまで改めて指定しないといけないのは冗長です。
そこでカリー化を行います。

例えば以下のようにfetchを書き換えて関数を返す関数(高階関数)とすることで、メソッドを固定して(部分適用して)、getpostといった関数を作成することもできます。

const fetch = (method: 'POST' | 'GET') => {
    return (url : string, param : {}) => {
      // 3種類の引数を使って非同期処理を実行...
    }
};

const post: (url : string, param : {}) => void = fetch('POST');
const get: (url : string, param : {}) => void = fetch('GET');

post('http://hogehogehogehoge', { value: 'xxxx'});
post('http://fugafugafugafuga', { value: 'xxxx'});
get('http://fugafugafugafuga', {});

さらにURLやパラメータについてもカリー化を行うことで、さらに細かく部分適用した関数を作成することができます。

const fetch = (method: 'POST' | 'GET') => (url: string) => (param: {}) => {
    return () => {
      // 3種類の引数を使って非同期処理を実行...
    }
}

const post = fetch('POST');
const get = fetch('GET');

const postHoge = post('http://hogehogehogehoge');
const postFuga = post('http://fugafugafugafuga');
const getFuga = get('http://fugafugafugafuga');

const postHogeXXXX = postHoge({value: 'xxxx'});
const postFugaXXXX = postFuga({value: 'xxxx'});
const getFugaNoParam = getFuga({});

postHogeXXXX();
postFugaXXXX();
getFugaNoParam();

どのレベルまでカリー化を行うかは実装と相談になるかと思いますが、特定の引数が固定になっているケースを多く見る場合はカリー化してみると便利かと思います。

まとめ

今回は「高階関数」と「カリー化」についてTypescriptのコードベースで解説を行いました。
ReactReactNativeで開発する場合、これらを応用した「高階コンポーネント」というものを使ったりします。
それはまた別の機会に解説したいと思います。

Discussion

standard softwarestandard software

カリー化のところ、この記事と似てますね。

高階関数、カリー化、部分適用 - Qiita
https://qiita.com/nouka/items/d9f29db7b6a69baa650a#comment-d0f4d534bd6f8becdf52

リンク先のコメントに記載してみましたが、カリー化は前方引数からしか部分適用できないので、あんまりメリットないと思います。

nekonikinekoniki

コメントありがとうございます。

カリー化のところ、この記事と似てますね。

たしかに似てますね。一部項目に関しては、教わった内容を噛み砕いて記事にしているので、参照元が同じだったのかもしれません。

リンク先のコメントに記載してみましたが、カリー化は前方引数からしか部分適用できないので、あんまりメリットないと思います。

おっしゃる通りで、私自身実際に使っているかというと「うーん・・・」といったところです。
具体的にメリットを享受できる場面は極めて限定的なのかなと思います。