【TypeScript】カリー化・部分適用は便利だよ!
カリー化・部分適用利用してますか?
調べたけど「関数を第一級オブジェクトとしてー」とか「関数を部分適用してー」とか説明が講義っぽくて途中で諦めた方も多いと思います。自分もそうでした。
また、知ってるけどどんな時に使うべきか迷って使ってないという方もいると思います。
具体的なコードを交えながら、カリー化・部分適用について、噛み砕いて説明していきたいと思います。
なお、すべてのTypeScriptのサンプルコードは実際に動かして確認することができますので、TypeScript Playgroundなどで是非お試しください。
カリー化と部分適用
まずはカリー化と部分適用の定義をば。
カリー化 (currying, カリー化された=curried) とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。
部分適用とは、複数の引数をとる関数の一部の引数に実引数を適用する操作のこと
はい、もう意味分からないですね。
誤解を恐れず意訳すると、
「この関数には引数が2つ必要だけど、とりあえず1個目の引数ちょうだい。もう1個はあとでちょうだい。そこからまた処理続けるから。」
という感じです。
// 普通の関数
const add = (x: number, y: number): number => x + y;
console.log(add(1, 2)); // 3
// カリー化された関数
const curriedAdd = (x: number) => (y: number) => x + y;
console.log(curriedAdd(1)(2)); // 3
const add1 = curriedAdd(1);
console.log(add1); // (y) => x + y
console.log(add1(2)); // 3
console.log(add1(3)); // 4
上記はよくあるサンプルコードですが、add1
を出力してみると(y) => x + y
という結果が得られます。
(y) => x + y
内にはx
を定義している箇所がありませんが、
それはconst add1 = curriedAdd(1)
の時点でx
には1
が入れられているからです。
なので、add1
の(y) => x + y
は、(y) => 1 + y
ということになります。
となると、
add1(2)
は、(2) => 1 + 2
で3となり、
add1(3)
は、(3) => 1 + 3
で4となるわけです。
先ほどの「引数が2つ必要だけど、とりあえず1個目の引数ちょうだい。もう1個はあとでちょうだい。」という意味がわかっていただけたかと思います。
これで簡単な説明は終わりですが、よく出てくる第一級関数とか高階関数とか、カリー化と部分適用は別物?とか、説明していきたいと思います。
また、最後に実際に使えそうなサンプルも書いていきます。
第一級関数
あるプログラミング言語が第一級関数 (First-class functions) を持つと言われる場合、その言語の関数がその他の変数と同様に扱われることを表します。
MDN Web Docs - First-class Function (第一級関数)
第一級関数(だいいっきゅうかんすう、英: first-class function、ファーストクラスファンクション)とは、関数を第一級オブジェクトとして扱うことのできるプログラミング言語の性質、またはそのような関数のことである。
これは説明の通り、関数を「その他の変数と同様に扱」うことができることを意味しています。
変数と同様に扱えるということは、生成や代入、受け渡しなどが変数と同じようにできるということです。
// 生成
let calc = (x: number, y: number): number => x + y;
// 代入
calc = (x: number, y: number): number => x - y;
// 受け渡し
const diffLength = (a: string, b: string, fn: (x: number, y: number) => number): number => {
return fn(a.length, b.length);
};
console.log(diffLength('aaa', 'a', calc)); // 2
高階関数
高階関数(こうかいかんすう、英: higher-order function)とは、第一級関数をサポートしているプログラミング言語において少なくとも以下のうち1つを満たす関数である。
- 関数(手続き)を引数に取る
- 関数を返す
先ほど第一級関数で見た通り、関数を変数と同様に扱えるため、引数や戻り値に関数を設定することができます。
この 「引数や戻り値に関数を設定」した関数を高階関数と呼びます。
先ほどとほぼ同様ですが、別のサンプルを挙げておきます。
const calculate = (x: number, y: number, operation: (x: number, y: number) => number): number => {
return operation(x, y);
};
const add = (x: number, y: number): number => x + y;
const subtract = (x: number, y: number): number => x - y;
console.log(calculate(5, 3, add)); // 8
console.log(calculate(5, 3, subtract)); // 2
改めて、カリー化と部分適用
Wikiにある通り、混同しやすい二つですが、カリー化=部分適用ではありません。
部分適用は引数の一部を適用することです。
先ほどの、const add1 = curriedAdd(1)
の時点でx
には1
が"適用"される、この操作のことを指します。
対して、カリー化は通常の関数からカリーな関数にすることを指します。
つまり、
const add = (x: number, y: number): number => x + y;
という関数から
const curriedAdd = (x: number) => (y: number) => x + y;
という関数に導くことカリー化と呼びます。
もっと簡単にいうと、引数を一個ずつ渡せるような関数にすること、とも言えると思います。
最初に挙げたサンプルをもう一度見てみましょう。
const add = (x: number, y: number): number => x + y;
// ↓ 普通の関数をカリーな関数に導く(カリー化)
const curriedAdd = (x: number) => (y: number) => x + y;
const add1 = curriedAdd(1); // 引数xに1が適用される(部分適用)
ここまで来れば、第一級関数・高階関数・カリー化・部分適用の関連も見えてくると思います。
関数をカリーな関数に導くためには、
- 関数を変数と同様に扱える必要がある(第一級関数)
- 関数を戻り値に設定する必要がある(高階関数)
- 引数のうち一部のみを適用できる必要がある(部分適用)
こんな感じでしょうか。
利用シーン
さて、仕組みがわかったところで、最後に簡単な利用シーンを考えてみましょう。
正直、const curriedAdd = (x: number) => (y: number) => x + y;
のようなサンプルでは実際の利用シーンに繋げにくいですよね。
例えば、Webスクレイピングをしていて、対象のページのテーブル構造が必ずしも毎回同じではないとします。
上記のテーブルをそれぞれRecordで取得したとすると下記のようになると思います。
const hoge = {
名前: 'ほげ太郎',
年齢: '30',
職業: 'エンジニア',
一言: 'よろしくお願いします!',
}
const fuga = {
名前: 'ふが子',
職業: 'デザイナー',
趣味: 'キャンプ',
}
取得できたレコードを元にユーザー情報を生成するとして、値があればその値、なければ空文字で扱いたいとします。
しかし、テーブルによって項目の有無があるためundefined
判定が必要になります。
(NoSQLなどで情報を取得した際でも同じような状況が想定できそうですね。)
const records: Record<string, string>[] = [
{
名前: 'ほげ太郎',
年齢: '30',
職業: 'エンジニア',
一言: 'よろしくお願いします!',
},
{
名前: 'ふが子',
職業: 'デザイナー',
趣味: 'キャンプ',
}
];
for (const rec of records) {
const user = {}
if (rec['名前']) user.name = rec['名前'];
else user.name = ''
if (rec['年齢']) user.age = rec['年齢'];
else user.age = ''
// ・・・
console.log(user);
}
項目分書くのは効率的ではなさそうです…。
値を取り出す処理を関数にしてみましょう。
const records: Record<string, string>[] = [
{
名前: 'ほげ太郎',
年齢: '30',
職業: 'エンジニア',
一言: 'よろしくお願いします!',
},
{
名前: 'ふが子',
職業: 'デザイナー',
趣味: 'キャンプ',
}
];
const fetchValue = (table: Record<string, string>, key: string): string => {
if (table[key]) return table[key];
return '';
}
for (const rec of records) {
const user = {
name: fetchValue(rec, '名前'),
age: fetchValue(rec, '年齢'),
job: fetchValue(rec, '職業'),
hobby: fetchValue(rec, '趣味'),
comment: fetchValue(rec, '一言'),
}
console.log(user);
}
だいぶスッキリしましたが、まだ改修できそうですね。
fetchValue
に毎回rec
を渡している部分、ここに部分適用ができそうです。
const records: Record<string, string>[] = [
{
名前: 'ほげ太郎',
年齢: '30',
職業: 'エンジニア',
一言: 'よろしくお願いします!',
},
{
名前: 'ふが子',
職業: 'デザイナー',
趣味: 'キャンプ',
}
];
// カリー化
// (key: string) => stringが戻り値の関数
const fetchValue = (table: Record<string, string>): (key: string) => string => {
return (key: string) => {
if (table[key]) return table[key];
return '';
}
}
for (const rec of records) {
// 一つ目の引数に部分適用
const record = fetchValue(rec);
const user = {
// 二つ目の引数を渡していく
name: record('名前'),
age: record('年齢'),
job: record('職業'),
hobby: record('趣味'),
comment: record('一言'),
}
console.log(user);
}
カリー化することで、rec
を部分適用し、ソースコードがスッキリさせることができました。
得られる結果は下記のようになると思います。
{
"name": "ほげ太郎",
"age": "30",
"job": "エンジニア",
"hobby": "",
"comment": "よろしくお願いします!"
}
{
"name": "ふが子",
"age": "",
"job": "デザイナー",
"hobby": "キャンプ",
"comment": ""
}
最後に
カリー化について簡単にまとめてみました。
少しでも皆様の理解に繋がれば幸いです。
もちろんご指摘等のマサカリ大歓迎です!