関数を返す関数
このレッスンで分かること
- 関数を受け取る関数と、関数を返す関数の違い
- 複数の変換をつなげて 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)); // 241 を 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; }]);合成した関数は何度も呼ばれます。中で外の値を書き換えると、呼んだ回数によって結果が変わります。次のレッスンでこの問題を扱います。
要件
- 関数名は buildTransform とし、引数は stepNames (文字列の配列) と values (数値の配列) の 2 つにする
- double は 2 倍、addTen は 10 を足す、square は 2 乗する。工程名と処理の対応を関数の中に用意する
- 工程名の配列から 1 本の関数を組み立てて返す関数を定義し、その戻り値を使って values を変換する
- 戻り値は 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"], []) → []