ウキウキ★コーディング講座のお時間です。有料です。嘘です。金返せ。

JavaScript(TypeScript)のGeneratorやらyieldやらIterableやらについて話すよ。C#erは前に書いたこの記事でもしゃぶっててね。

◆例題

ひとつのオブジェクトを受け取って配列を返す関数をつくる

ってケースについて考えたいので、以下の例題を設定する。

- エラーか否かを各プロパティに持ってるオブジェクトがある。
- そのインスタンスを受け取り、エラーメッセージを構築する。

▼オブジェクトの構造

{
  "hoge": "error",
  "fuga": "error",
  "piyo": "error",
  "unko": "non-error"
}

"error"になってるやつはアレしたい。下の感じのエラーを出したい。

・hogeがエラーです。
・fugaがエラーです。
・piyoがエラーです。

「オブジェクトのプロパティを読んでエラーメッセージ配列を返す」ようなfunctionについて考える。

▼よくあるやつ

JavaScriptで書いてみる。

function formatError(errors) {
  const errorMessages = [];
  if (errors.hoge === 'error') errorMessages.push('・hogeがエラーですよ。');
  if (errors.fuga === 'error') errorMessages.push('・fugaがエラーですよ。');
  if (errors.piyo === 'error') errorMessages.push('・piyoがエラーですよ。');
  if (errors.unko === 'error') errorMessages.push('・unkoがエラーですよ。');
  return errorMessages;
};

function main() {
  const errors = {
    hoge: 'error',
    fuga: 'error',
    piyo: 'error',
    unko: 'non-error',
  };

  const errorMessages = formatError(errors);
  console.log(errorMessages);
};
main();

これがまぁ普通だわな?

「formatError」メソッドの中で新しい配列を作って、そこにエラーメッセージをシコシコとpushして、出来上がった結果を返却する。

◆Generatorとyield

実は、formatError関数の中で配列を定義する必要はありません。というのも…あれ、説明難しいな。

えー

「途中でreturnして、もっかい呼んだらその続きから再開してまたreturnできるfunction」

を作ることができるんですね。うん。そういうのをJavaScript界隈ではジェネレーター関数と呼んでおる。

▼書き方

以下に変更。

  • function ⇒ function*
  • return ⇒ yield

しつつ、配列を無くして、あの…アレする。

function* formatError(errors) {
  if (errors.hoge === 'error') yield '・hogeがエラーですよ。';
  if (errors.fuga === 'error') yield '・fugaがエラーですよ。';
  if (errors.piyo === 'error') yield '・piyoがエラーですよ。';
  if (errors.unko === 'error') yield '・unkoがエラーですよ。';
};

function main() {
  const errors = {
    hoge: 'error',
    fuga: 'error',
    piyo: 'error',
    unko: 'non-error',
  };

  const errorMessages = formatError(errors);
  console.log(errorMessages);
};
main();

ね?

ただしこの出力結果はエラーになっちゃうんで、

console.log(Array.from(errorMessages));

みたいな感じでArray.from使って配列に変換したればいいよ。

…結局配列にしてんじゃねぇか!

▼解説

C#を知っていればもう「IEnumerable<T>を無能にした感じです」で話は終わりなんだけどなぁ。内容的に全くの別物であるとも言えるけど。

「function*」ってのがジェネレーターと呼ばれるfunctionを定義するときの記法です。ジェネレーターが何をジェネレートしてるかといえば、イテラブルなオブジェクトをジェネレートしている。

イテラブルは「反復可能」ということ。繰り返し値が取れるオブジェクト。身近な例でいえば配列もイテラブル。for-ofに食わせたらひとつずつ値を触れるだろ?

イテラブルである条件は「@@iterator」の関数を持っていること。Arrayも@@iterator関数を持っている

yieldによって値を返却しつつその場所を記憶させておいて、次の行から処理を再開できる。

例えばSetオブジェクトのコンストラクタは

new Set([iterable])

です。イテラブルを食わすことできるんすね。

細かい話は端折ってるけど、そんな理解をしておこうよ。

▼TypeScript

type errorStatus = 'error' | 'non-error';
type Errors = { hoge: errorStatus; fuga: errorStatus; piyo: errorStatus; unko: errorStatus; };

function* formatError_ts(errors: Errors): Generator<string> {
  if (errors.hoge === 'error') return '・hogeがエラーですよ。';
  if (errors.fuga === 'error') return '・fugaがエラーですよ。';
  if (errors.piyo === 'error') return '・piyoがエラーですよ。';
  if (errors.unko === 'error') return '・unkoがエラーですよ。';
};

function main_ts() {
  const errors: Errors = {
    hoge: 'error',
    fuga: 'error',
    piyo: 'error',
    unko: 'non-error',
  };

  const errorMessages = formatError(errors);
}
main_ts();

こんなん感じで。

型はここを参照してみてね。returnの型だけ変えたりできるよ。

▼うまあじ&まずあじ

インスタンスひとつだったらifでエラーメッセージ組んだとしても問題にならんだろう。が、このオブジェクトの配列に対してエラーメッセージを構築せんと思ひて、さらに重複除去とかしつかまつらんとしたら面倒だわな。

そういうこと。ほかには以下の感じ。

  • 配列いらないし分かりやすいね!
    • これチームのメンバーが読んで理解できるの?
      • できなくね?
  • flatMapくんがイテレーターを分解してくれない。
    • これひどい。内部でconcatを使っとるんだとさ。
    • とはいえ、これ分解するようにしちゃったら破壊的変更ですよね。
    • 死んで💕
  • IEnumerableとは違って、Array.prototypeに定義されてるmapとかの便利メソッドは使えないよ。[...errorMessages]やらArray.from(errorMessages)しないとだめだよ。

いやはや。ジェネリクスは大した発明だ(遠い目)。C#なら何も考えずにLINQ使えばいいというのに…。っら…Blazorしょ…

◆そのた

yield* とかの記法もあるんだけど、もうMDN見に行けよ。

function* 宣言 | MDN

非同期を殴れるasyncIteratorもあるんだけど、MDN見に行けよ。

for await...of | MDN

◆結

コンテナやらインフラについての技術記事書きたいんだけど、そもそもの話をしまくりつつ脱線しまくって爆発して憤死する気がしていて書けない。