uhyo さんのジェネレータ関数についての記事に触発されました。
僕もたびたび実務でジェネレータ関数を活用しているので、僕からも軽い TIPS を共有しておきます。
結論
このような generateArray
関数を作っておくと、「ジェネレータ関数を使って配列を作る」処理が楽に書けます。
プロジェクト全体で利用可能な汎用ユーティリティとして用意しましょう。
src/utils/generateArray.ts
/**
* 渡されたジェネレータ関数を実行して、配列にする。
*/
export const generateArray = T>(
generatorFn: () => GeneratorT, void>,
): T[] => {
return Array.from(generatorFn());
};
ネストの深さが減る
標準の Array.from
だけで書く場合(Before)と、 generateArray
(After)を使う場合を見比べてみましょう。
Prettier を使ってフォーマットすると、以下のようになります。
// Before
const nums1 = Array.fromnumber>(
(function* () {
yield 1;
yield* [2, 3];
})(),
);
// After
const nums2 = generateArraynumber>(function* () {
yield 1;
yield* [2, 3];
});
Before だと yield
の場所が3段階目になっていますが、After では 2段階目になっています。
ネストの深さが減るだけでも、全体の構造をつかむのが少しラクになると思います。
脳内スタックの深さも減る
しかし、それだけではありません。読む人の脳内の「スタック」 の深さも減ります。
Before では IIFE (即時実行関数式, (function *() { /* ... */ })()
)を使っていますが、 After では不要になっています(generateArray
がジェネレータ関数を実行してくれるからです。)
即時実行関数式は、コードを読む人にとっては負担がそこそこ大きいイディオムです。
コードを上から順に読み進めるときに、
// Before
const nums1 = Array.fromnumber>(
(function* () {
// ^ お、ここから関数やな
yield 1;
yield* [2, 3];
})(),
// ^ お、関数の後ろに `()` がついてるから、
// この式全体の結果は「関数型そのもの」じゃなくて、
// 「関数の返り値」になるんやな
);
という順をたどります。
つまり、「これは結局『関数そのもの』なのか?それとも『関数の実行結果の値』なのか?」という Ambiguity(両義性, いわゆる「あいまいさ」)を抱えたまま読み進めて、最後の ()
に到達してやっと カタルシスを得る Ambiguity が解消されることになります。
コード例はジェネレータ関数の中身が2行だけなのでカワイイものですが、このジェネレータ関数が長大になるにつれて負荷が大きくなるのは、容易に想像していただけると思います。
generateArray
のような形のユーティリティ関数には、この IIFE による「読むときの負担」を軽減する効果もあるのです。
同様の効果が得られる、既存の有名なユーティリティ等の例:
誤returnも防止できる
オマケとして、generateArray
は、型による「良い感じのガードレール」として機能してくれます。
一度 generateArray
ユーティリティをつくてしまえば、その使用者は Generator
型の詳しい知識不要でこれを使えるのでお得です。
Before だと「number の配列を作ろうとしてたのに、違う型の値を yield
したらダメ」程度の検証しかしてくれませんが、それと比べると少し厳しくしてくれます。
ジェネレータ関数で配列を単に生成するとき、
return;
は文字通り早期リターン、つまり「配列の生成はこれで終わり」とけりをつけて終了するのに使えますが、
return 1;
のように書いても、その 1
という値は特に何の利用価値もありません。むしろ yield
すべきだったのに書き間違えた可能性があります。
const nums2 = generateArraynumber>(function* () {
yield 1;
if (isHoge) {
return; //
}
yield* [2, 3];
return 1; //
});
generateArray
は、その型宣言によって、return;
は許可しつつ、 return 1;
には以下のような型エラーを提供してくれます。
型
() => Generator
の引数を型() => Generator
のパラメーターに割り当てることはできません。
この型エラーは、引数の generatorFn
について、関数の型を明示して「この関数が値を return しない」という制約を与えているから、と考えられます。
(再掲)src/utils/generateArray.ts
/**
* 渡されたジェネレータ関数を実行して、配列にする。
*/
export const generateArray = T>(
generatorFn: () => GeneratorT, void>,
): T[] => {
return Array.from(generatorFn());
};
GeneratorT, void>,
// ^ 関数がジェネレータ関数であること(必要ではないが十分条件として)
// ^ T: yield する値の型
// ^ TReturn: void なので「値を返さない」
// ^ 第3引数 TNext は省略 -> any
正直、僕もジェネレータの全てを理解しているわけではないので、今回使わなかった第3引数の TNext
の使い所とかよくわかりませんでしたが、だれかが書いてくれていると期待して閉じます。
おしまい。
Views: 1