関数を返す関数

このレッスンで分かること

  • 関数を受け取る関数と、関数を返す関数の違い
  • 複数の変換をつなげて 1 本の関数に合成するやりかた
  • 既存の関数を包んで、あとから振る舞いを足す書きかた

関数を返す関数 とは

戻り値が関数になっている関数のことです。処理そのものを組み立てて渡せるようになり、変換の順番や回数をデータとして扱えます。

受け取る側と返す側

第2章で map や filter に関数を渡しました。あれは「関数を受け取る関数」です。前のレッスンで書いた工場は「関数を返す関数」でした。まとめるとこうなります。

種類何が便利か
関数を受け取るmap, filter, sort処理の中身を呼ぶ側が決められる
関数を返すcreateMinViewsFilter設定を先に決めて使い回せる
受け取って返す合成, 包み既にある関数から新しい関数を組み立てられる

どちらの性質を持つ関数も高階関数と呼びます。このレッスンで扱うのは 3 行目です。すでにある関数を材料にして、新しい関数を作ります。

2 つの関数をつなげる

表示回数を 2 倍にする関数と、10 を足す関数があるとします。両方を順に適用したい場面は珍しくありません。

function double(n) { return n * 2; } function addTen(n) { return n + 10; } console.log(addTen(double(5))); // 20

これでも動きますが、map に渡すときに無名関数で包み直すことになります。かわりに、2 つの関数から 1 本の関数を作る関数を用意します。

function compose(first, second) { return function (value) { return second(first(value)); }; } const doubleThenAddTen = compose(double, addTen); console.log([1, 2, 3].map(doubleThenAddTen)); // [12, 14, 16]

compose が返すのはただの関数なので、map にそのまま渡せます。first と second はクロージャに閉じ込められていて、外から差し替えられません。

順番が結果を決めます。compose(double, addTen) と compose(addTen, double) は別物です。5 を渡すと前者は 20、後者は 30 になります。

何本でもつなげる

つなげたい関数が 2 本とは限りません。配列で受け取れば、本数に制限がなくなります。

function composeAll(fns) { return function (value) { let result = value; for (const fn of fns) { result = fn(result); } return result; }; } const transform = composeAll([double, addTen, double]); console.log(transform(1)); // 24

1 を 2 倍して 2、10 を足して 12、もう一度 2 倍して 24 という順です。空の配列を渡した場合は for が 1 回も回らず、値がそのまま返ります。これは「何もしない変換」として扱えるので、条件分岐を書かずに済みます。

工程の名前が文字列で来る場合は、名前と関数の対応表を用意します。

const steps = { double: double, addTen: addTen, square: function (n) { return n * n; } }; function buildTransform(names) { return composeAll(names.map(function (name) { return steps[name]; })); } console.log([2, 3].map(buildTransform(["square", "addTen"]))); // [14, 19]

処理の順番が ["square", "addTen"] という配列になりました。ここまで来ると、並び順を画面のフォームから受け取ることもできます。今回の課題はこの形を書きます。

作品の表示に当てはめる

数値だけでなく、作品オブジェクトの変換にも同じ形が使えます。作品を受け取って作品を返す関数を並べれば、表示用のデータを組み立てる流れが 1 本の関数になります。

function markFeatured(work) { return { ...work, featured: work.views >= 100 }; } function shortenTitle(work) { const title = work.title.length > 6 ? work.title.slice(0, 6) : work.title; return { ...work, title: title }; } const toCardData = composeAll([markFeatured, shortenTitle]); const works = [ { id: 1, title: "プロフィールサイト", views: 120 }, { id: 2, title: "自己紹介カード", views: 48 } ]; console.log(works.map(toCardData));

markFeatured も shortenTitle も、受け取る形と返す形が同じ作品オブジェクトです。形がそろっていれば何本でも並べられ、順番を入れ替えても壊れません。処理を足したくなったら composeAll に渡す配列に 1 行足すだけになります。

逆に、途中で数値を返す関数を混ぜると、そこから先の関数が work.views を読めずに落ちます。合成できる関数は「入口と出口の形がそろっているもの」だけだ、と押さえておきます。

既にある関数を包む

関数を返す関数のもうひとつの使い道が、既存の関数に振る舞いを足すことです。中身を書き換えずに外から包みます。

function withZeroGuard(fn) { return function (value) { if (value === 0) { return 0; } return fn(value); }; } const safeSquare = withZeroGuard(function (n) { return n * n; }); console.log(safeSquare(0)); // 0 console.log(safeSquare(4)); // 16

元の関数はそのまま残っているので、包む前の動きも必要なら使えます。第6章のエラー処理や第8章の再試行も、この形で書けます。包む関数を何種類か用意しておくと、組み合わせで振る舞いを増やせます。

包む関数は、受け取る関数の引数と戻り値の形をそろえて返します。形が変わると、渡し先の map や filter が期待するものと合わなくなります。

よくある間違い

ひとつめは、関数を返すつもりで結果を返してしまうことです。

function compose(first, second) { return second(first(value)); // value がまだ無い }

合成した時点では入力値がありません。返すのは値ではなく関数です。return function (value) から書き始めます。

ふたつめは、対応表から関数を取り出すときに括弧を付けてしまうことです。

const fns = names.map(function (name) { return steps[name](); // ここで実行してしまっている });

これでは実行結果が配列に入ります。渡したいのは関数そのものなので、括弧は付けません。

みっつめは、合成した関数の中で外側の変数を書き換えることです。

let total = 0; const addToTotal = composeAll([function (n) { total = total + n; return n; }]);

合成した関数は何度も呼ばれます。中で外の値を書き換えると、呼んだ回数によって結果が変わります。次のレッスンでこの問題を扱います。

要件

  1. 関数名は buildTransform とし、引数は stepNames (文字列の配列) と values (数値の配列) の 2 つにする
  2. double は 2 倍、addTen は 10 を足す、square は 2 乗する。工程名と処理の対応を関数の中に用意する
  3. 工程名の配列から 1 本の関数を組み立てて返す関数を定義し、その戻り値を使って values を変換する
  4. 戻り値は values と同じ長さの数値の配列にする。stepNames が空のときは values の値をそのまま返す

入出力例

buildTransform(["double"], [1,2,3])[2,4,6] buildTransform(["double","addTen"], [0,5])[10,20] buildTransform(["addTen","double"], [0,5])[20,30] buildTransform([], [3,4])[3,4] buildTransform(["square"], [])[]

ヒント

生田 陸人
ゆめさくエンジニア / 現役ソフトウェアエンジニア
編集 LuaGate編集部
main.js
main.js
学習モード

メモ

関数を返す関数

⌘S で保存