17
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScriptジェネレータ関数とユーティリティで楽に配列を生成する

Last updated at Posted at 2025-06-06

uhyo さんのジェネレータ関数についての記事に触発されました。

僕もたびたび実務でジェネレータ関数を活用しているので、僕からも軽い TIPS を共有しておきます。

結論

このような generateArray 関数を作っておくと、「ジェネレータ関数を使って配列を作る」処理が楽に書けます。

プロジェクト全体で利用可能な汎用ユーティリティとして用意しましょう。

src/utils/generateArray.ts
/**
 * 渡されたジェネレータ関数を実行して、配列にする。
 */
export const generateArray = <T>(
  generatorFn: () => Generator<T, void>,
): T[] => {
  return Array.from(generatorFn());
};

ネストの深さが減る

標準の Array.from だけで書く場合(Before)と、 generateArray (After)を使う場合を見比べてみましょう。

Prettier を使ってフォーマットすると、以下のようになります。

// Before
const nums1 = Array.from<number>(
  (function* () {
    yield 1;
    yield* [2, 3];
  })(),
);
// After
const nums2 = generateArray<number>(function* () {
  yield 1;
  yield* [2, 3];
});

Before だと yield の場所が3段階目になっていますが、After では 2段階目になっています。

ネストの深さが減るだけでも、全体の構造をつかむのが少しラクになると思います。

脳内スタックの深さも減る

しかし、それだけではありません。読む人の脳内の「スタック」 の深さも減ります。

Before では IIFE (即時実行関数式, (function *() { /* ... */ })())を使っていますが、 After では不要になっています(generateArray がジェネレータ関数を実行してくれるからです。)

即時実行関数式は、コードを読む人にとっては負担がそこそこ大きいイディオムです。

コードを上から順に読み進めるときに、

// Before
const nums1 = Array.from<number>(
  (function* () {
    // ^ お、ここから関数やな
    yield 1;
    yield* [2, 3];
  })(),
  // ^ お、関数の後ろに `()` がついてるから、
  //   この式全体の結果は「関数型そのもの」じゃなくて、
  //   「関数の返り値」になるんやな
);

という順をたどります。

つまり、「これは結局『関数そのもの』なのか?それとも『関数の実行結果の値』なのか?」という Ambiguity(両義性, いわゆる「あいまいさ」)を抱えたまま読み進めて、最後の () に到達してやっと カタルシスを得る Ambiguity が解消されることになります。

コード例はジェネレータ関数の中身が2行だけなのでカワイイものですが、このジェネレータ関数が長大になるにつれて負荷が大きくなるのは、容易に想像していただけると思います。

generateArray のような形のユーティリティ関数には、この IIFE による「読むときの負担」を軽減する効果もあるのです。

同様の効果が得られる、既存の有名なユーティリティ等の例:

誤returnも防止できる

オマケとして、generateArray は、型による「良い感じのガードレール」として機能してくれます。

一度 generateArray ユーティリティをつくてしまえば、その使用者は Generator<T, TReturn, TNext> 型の詳しい知識不要でこれを使えるのでお得です。

Before だと「number の配列を作ろうとしてたのに、違う型の値を yield したらダメ」程度の検証しかしてくれませんが、それと比べると少し厳しくしてくれます。

ジェネレータ関数で配列を単に生成するとき、

return; は文字通り早期リターン、つまり「配列の生成はこれで終わり」とけりをつけて終了するのに使えますが、

return 1; のように書いても、その 1 という値は特に何の利用価値もありません。むしろ yield すべきだったのに書き間違えた可能性があります。

const nums2 = generateArray<number>(function* () {
  yield 1;
  if (isHoge) {
    return; // <- 正当な早期 return
  }
  yield* [2, 3];
  return 1; // <- 誤った return 
});

generateArray は、その型宣言によって、return; は許可しつつ、 return 1; には以下のような型エラーを提供してくれます。

() => Generator<number, number, any> の引数を型 () => Generator<number, void, any> のパラメーターに割り当てることはできません。

この型エラーは、引数の generatorFn について、関数の型を明示して「この関数が値を return しない」という制約を与えているから、と考えられます。

(再掲)src/utils/generateArray.ts
/**
 * 渡されたジェネレータ関数を実行して、配列にする。
 */
export const generateArray = <T>(
  generatorFn: () => Generator<T, void>,
): T[] => {
  return Array.from(generatorFn());
};
Generator<T, void>,
// ^ 関数がジェネレータ関数であること(必要ではないが十分条件として)
//        ^ T: yield する値の型
//           ^ TReturn: void なので「値を返さない」
//               ^ 第3引数 TNext は省略 -> any

正直、僕もジェネレータの全てを理解しているわけではないので、今回使わなかった第3引数の TNext の使い所とかよくわかりませんでしたが、だれかが書いてくれていると期待して閉じます。

おしまい。

17
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?