第04章:カリー化
ないと生きられないもの
父がかつて『手に入れるまで必要ないものが存在する』と説明していました。電子レンジもその一つ。スマートフォンも同様。年配の方々はインターネットなくても充実した生活を送っていたことを思い出されるでしょう。私にとってカリー化もこのリストに入ります。
コンセプトはシンプルです。関数が必要とする引数より少ない数で呼び出した場合、残りの引数を受け取る新しい関数を返します。
一括で全ての引数を渡すか、個別に引数を供給することが可能です。
const add = x => y => x + y;
const increment = add(1);
const addTen = add(10);
increment(2); // 3
addTen(2); // 12ここでは引数を1つ取り、関数を返すadd関数を作成しました。これを呼び出すことで、返される関数はクロージャーを通じて最初の引数を記憶します。ただし両方の引数を一度に渡すのは面倒なので、curryヘルパー関数を使用して定義と呼び出しを容易にできます。
カリー化関数をいくつか作成してみましょう。以降は付録A - 必須関数サポートで定義したcurry関数を使用します。
const match = curry((what, s) => s.match(what));
const replace = curry((what, replacement, s) => s.replace(what, replacement));
const filter = curry((f, xs) => xs.filter(f));
const map = curry((f, xs) => xs.map(f));採用したパターンはシンプルだが重要です。操作対象のデータ(String、Array)を最後の引数に配置しています。実際に使ってみると、その理由が自然と理解できるでしょう。
(構文/r/gは全ての'r'文字にマッチする正規表現です。正規表現の詳細を参照してください)
match(/r/g, 'hello world'); // [ 'r' ]
const hasLetterR = match(/r/g); // x => x.match(/r/g)
hasLetterR('hello world'); // [ 'r' ]
hasLetterR('just j and s and t etc'); // null
filter(hasLetterR, ['rock and roll', 'smooth jazz']); // ['rock and roll']
const removeStringsWithoutRs = filter(hasLetterR); // xs => xs.filter(x => x.match(/r/g))
removeStringsWithoutRs(['rock and roll', 'smooth jazz', 'drum circle']); // ['rock and roll', 'drum circle']
const noVowels = replace(/[aeiou]/ig); // (r,x) => x.replace(/[aeiou]/ig, r)
const censored = noVowels('*'); // x => x.replace(/[aeiou]/ig, '*')
censored('Chocolate Rain'); // 'Ch*c*l*t* R**n'ここで示しているのは、引数を1つまたは2つ「事前読み込み」して、それらの引数を記憶する新しい関数を取得する能力です。
Mostly Adequateリポジトリをクローン(git clone https://github.com/MostlyAdequate/mostly-adequate-guide.git)して、上記コードをREPL(対話型評価環境)で試すことを推奨します。付録で定義されたcurry関数やその他の機能はsupport/index.jsモジュールで利用可能です。
またはnpmで公開されているバージョンを参照:
npm install @mostly-adequate/support洒落以上に役立つ特別な調味料
カリー化は様々な用途に有用です。hasLetterR、removeStringsWithoutRs、censoredで見られるように、ベース関数に引数を渡すだけで新規関数を作成可能です。
mapでラップするだけで、単一要素を扱う関数を配列操作関数に変換できます:
const getChildren = x => x.childNodes;
const allTheChildren = map(getChildren);関数の期待値より少ない引数を与えることを部分適用と呼びます。部分適用は定型コードを大幅に削減できます。アンカリー化されたlodashのmapを使った場合のallTheChildren関数を比較してみましょう(引数の順序が異なります):
const allTheChildren = elements => map(elements, getChildren);通常、配列操作関数を定義しません。map(getChildren)をインラインで呼び出せるためです。これはsortやfilter、その他の高階関数(高階関数は関数を引数に取る/返す関数)にも当てはまります。
純粋関数について議論した際、1入力1出力の特性を説明しました。カリー化はまさにこれを実現します:各引数が残りの引数を期待する新関数を返すためです。これが真の1入力1出力です。
出力が別の関数でも純粋であることに変わりありません。複数引数を一度に許可しますが、これは単に便利のため余分な()を削除しているに過ぎません。
まとめ
カリー化は実用性が高く、日常的にカリー化関数を使うことを非常に楽しんでいます。関数型プログラミングの冗長さを軽減する有用なツールです。
引数をいくつか渡すだけで新規の有用な関数を即座に作成可能で、複数引数を持ちながら数学的関数定義を維持できる利点があります。
次に重要なツールcomposeを習得しましょう。
演習問題
演習に関する注意
本書ではこのような『演習』セクションが登場します。gitbook(推奨)で閲覧中の場合はブラウザ内で直接解答可能です。
すべての演習問題では、付録A、付録B、付録Cで定義されたヘルパー関数がグローバルスコープで利用可能です。さらに特定の問題専用の関数が定義されている場合もあるため、それらも利用可能とみなして問題ありません。
ヒント:エディタで
Ctrl + Enterを押すと解答を提出できます。
自分でマシン上で演習を実行する(オプション)
任意のエディタでファイル上で演習を実施する場合:
- リポジトリをクローン(
git clone git@github.com:MostlyAdequate/mostly-adequate-guide.git) - 演習セクションに移動(
cd mostly-adequate-guide/exercises) - node v10.22.1を使用(例:
nvm install)。詳細は書籍のREADME参照 - npmで必要環境をセットアップ(
npm install) - 該当章のフォルダ内*exercise_**ファイルを編集して解答
- npmでテスト実行(例:
npm run ch04)
単体テストが解答を検証し、誤りがあればヒントを提供します。解答例は*solution_*ファイルに含まれています。
実践開始!
練習開始!
部分適用を使用して関数から全ての引数を取り除くようリファクタリングせよ。 const words = str => split(' ', str);
練習開始!
部分適用を使用して関数から全ての引数を取り除くようリファクタリングせよ。 const filterQs = xs => filter(x => match(/q/i, x), xs);
次の関数を考慮してください:
const keepHighest = (x, y) => (x >= y ? x : y);練習開始!
ヘルパー関数keepHighestを使用してmaxが引数を参照しないようリファクタリングせよ。
const max = xs => reduce((acc, x) => (x >= acc ? x : acc), -Infinity, xs);