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

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

https://qiita.com/uhyo/items/ee7fb36c050a934a345f

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

## 結論

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

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

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

## ネストの深さが減る

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

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

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

```ts
// After
const nums2 = generateArray<number>(function* () {
  yield 1;
  yield* [2, 3];
});
```

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

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

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

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

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

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

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

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

という順をたどります。

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

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

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

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

- [Remeda](https://remedajs.com/) の `pipe` 関数
- [React の `useMemo` フック](https://ja.react.dev/reference/react/useMemo)
:::


## 誤returnも防止できる

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

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

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

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

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

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

```tsx
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 しない」という制約を与えているから、と考えられます。

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

```ts
Generator<T, void>,
// ^ 関数がジェネレータ関数であること（必要ではないが十分条件として）
//        ^ T: yield する値の型
//           ^ TReturn: void なので「値を返さない」
//               ^ 第3引数 TNext は省略 -> any
```

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

おしまい。