きみはねこみたいなにゃんにゃんなまほう

ねこもスクリプトをかくなり

JavaScript の Symbol から Async Iterator に至る道

(=˘ ꒳ ˘=) 最近 Async Iterator ってなんじゃらほいで理解がストップすることが多い...

...あたりの投稿で Async Iterator についてちゃんと理解したいと思ったので、主にMDNをソースにお勉強していきます。

Symbol について

Iterator について調べていると Symbol というものをよく見かけるので、Iterator の前に調べていきます。

Symbol(key) というクラスっぽい関数があり、キーとなる文字列を引数に symbol という JavaScript のプリミティブなデータ型を生成し、symbol はオブジェクトのプロパティのキーに使えるみたいです。Symbol にはいくつかの static プロパティとしての symbol"well known" symbols として定義されており Symbol.iterator も "well know" symbol の一つということです。

なので、Iterable なオブジェクトの Iterator を取得するためのキーに Symbol.iterator が利用されている、ということですね。

Iterator について

Async Iterator を理解する前にまずは普通の Iterator を理解していないと気持ちが悪いですね。

簡単なコードで実験してみます。

const a = [1, 2, 3]
const iterator = a[Symbol.iterator]() // 関数を評価してイテレータを取得

iterator.next() // => { value: 1, done: false }
iterator.next() // => { value: 2, done: false }
iterator.next() // => { value: 3, done: false }
iterator.next() // => { value: undefined, done: true }
iterator.next() // => { value: undefined, done: true }

初め理解しづらかったのが、イテレータを取得するには a[Symbol.iterator] ではなく a[Symbol.iterator]() と関数の評価値として取得しなくてはならないということです。つまり配列の [Symbol.iterator] にはイテレータのファクトリ関数が格納されているということですね。

イテレータnext() が呼ばれるたびに返す値を返すというステートフルな振る舞いをすることを考えると、同一のインスタンスを参照してしまうと不都合がありそうなので、当然と言えば当然の仕様なのでしょうか。

以上を踏まえると for..of の振る舞いは

  1. of..[Symbol.iterator] を関数評価してイテレータを取得
  2. donetrue になるまでイテレータnext() しながらループを回す

と解釈できますね。

Iterable Protocol と Iterator Protocol

Iteration protocols - JavaScript | MDN

によると [Symbol.iterator]next() についての定義が Iterable Protocol と Iterator Protocol という2段階のプロトコルによって規定されています。

  • Iterable が満たすべき仕様(Iterator Protocol)
    • Iterator Protocol を満たすオブジェクトを返す関数を [Symbol.iterator] に定義せよと規定されている
    • これを満たすオブジェクトで for..of を適用可能
  • Iterator が満たすべき仕様(Iterator Protocol)
    • next の評価で返すべき値が規定されている

なので用語の使い分けを意識して一言で言うと for..of を適用可能な iterable とは iterator[Symbol.iterator]() で取得できるオブジェクト」 ということになります。

Iterator と Generator

イテレータと合わせて語られることが多いのが function*..yield で定義される Generator です。

Iterators and generators - JavaScript | MDN

によると Generator を使うと Iterator を実装しやすい、と説明されており確かに next() で値を取得できたりなど、ほぼそのまま Iterator として利用が可能な仕様になっています。

ただ、単純に実装の相性が良いという以上の意味合いはなさそうなので、クラスで Iterator を実装するということも十分できそうですね。

Async Iterator について

ようやく本題の Async Iterator です。しかし MDN には記事はないですね。

探して見たところ GitHub - tc39/proposal-async-iteration: Asynchronous iteration for JavaScript というところに仕様が存在するようです。このTC39というのがECMAScriptの標準化団体なんですね。

ただ...ちょっと難しすぎて...読めない。

しょうがないので TypeScript を見ながら理解していくことにします。型情報はシンプルかつ無敵の情報源です。

まずは Async Iterator から見ていきます。

interface AsyncIterator<T> {
  next(value?: any): Promise<IteratorResult<T>>;
  return?(value?: any): Promise<IteratorResult<T>>;
  throw?(e?: any): Promise<IteratorResult<T>>;
}

で、Iterator の定義が

interface Iterator<T> {
    next(value?: any): IteratorResult<T>;
    return?(value?: any): IteratorResult<T>;
    throw?(e?: any): IteratorResult<T>;
}

なので、Async Iterator は各メソッドの戻り値が async 化しただけの Iterator ですね。 この Async Iterator を実装するには Async Generator を利用するといいそうです。

以上を踏まえて単純なカウントアップを行うサンプルコードを書いてみました。

// 未定義だと for..await..of がイテレータにアクセスできないので asyncIterator シンボルを定義
;(Symbol as any).asyncIterator = Symbol.for('Symbol.asyncIterator')

// カウントアップする Async Generator を定義
async function* upCounter() {
  var sec = 0;
  while (true) {
    await delay(1000)
    yield sec++
  }
}

// おやすみ関数 (*˘꒳˘*) スヤァ...
function delay(msec: number) {
  return new Promise(resolve => setTimeout(resolve, msec))
}

// [Symbol.iterator] のマネして定義してみる (*˘꒳˘*) てきとう
const iterable = { [Symbol.asyncIterator]: upCounter }

;(async () => {
  for await (const x of iterable) {
    console.log(x)
  }
})()

これを実行すると

1
2
:

のようにカウントアップが開始します。

Async Generator の動作を確認してみる

通常の Generator の使い方から推測して

async function* upCounter() {
  var sec = 0;
  while (true) {
    await delay(1000)
    yield sec++
  }
}

と定義して使えてしまった Async Generator ですが、実際私が定義したものはなんだったのか、以下

const gen = upCounter()

として色々な呼び出し方を試してみます。

通常の Generator っぽくイテレーション(定数回)

まずはお馴染み、コピペ的に Generator を定数回呼び出すパターンです。

console.log(gen.next())
console.log(gen.next())
console.log(gen.next())

呼び出した回数だけ Promisee が帰ってきます。

Promise { <pending> }
Promise { <pending> }
Promise { <pending> }

これを then で取り出してみると。

gen.next().then(console.log)
gen.next().then(console.log)
gen.next().then(console.log)

解決済みの値が取得できます。

{ value: 0, done: false }
{ value: 1, done: false }
{ value: 2, done: false }

素朴な呼び出し方ですが、一応これでも値は取得できることがわかりましたね。

Promise.then を再帰呼び出ししてイテレーション

次に、Promise の走査ということで、少し無理矢理感がありますが再帰処理を利用したイテレーションを書いてみます。

const iterate = res => {
  if (res.done) return
  console.log(res.value) // <- 値を利用した何かしらの処理
  return gen.next().then(iterate)
}

gen.next().then(iterate)

これで

{ value: 0, done: false }
{ value: 1, done: false }
{ value: 2, done: false }
:

のように値が無限に取り出せるようになりました。同様の結果となるため以降の例では出力結果を省略します。

async 中の whileイテレーション

平易なループで非同期処理が記述できるという async の有り難みがわかりますね。

;(async () => {
  let res = await gen.next()
  while (!res.done) {
    console.log(res.value)
    res = await gen.next()
  }
})()

async 中の forイテレーション

単純な while から for への書き換えです。

;(async () => {
  for (let res = await gen.next(); !res.done; res = await gen.next()) {
    console.log(res.value)
  }
})()

async 中の for..ofイテレーション

以下のようなものを始め頭に浮かべたのですが、for..of のロジックの中で await 処理を絡める手段がなく、断念しました。

(async () => {
  const iterable = { [Symbol.iterator]: () => gen }
  for (const value of iterable) {
    console.log(await value)
  }
})()

for..of ではループのペイロード valueIterator が返す { value, done } から取得される想定なので Promise 化された値を返す Async Iterator とは微妙にインタフェースが合致しないんですね。

つまり Async Generator とは next().value で値を取得できることを Generator の規定とするならば厳密には Generator ではなく、よって Iterator でもない。そして Async Iterator である

と言えることになります。

async 中の for..await..ofイテレーション

というわけで for..await..of です。

(async () => {
  const iterable = { [Symbol.asyncIterator]: () => gen }
  for await (const value of iterable) {
    console.log(value)
  }
})()

Async Iterator を自作してみる

結局 next()Promise{ value, done } を返すというインタフェースさえ満たしてしまえば Async Iterator としては機能するということですね。

今回の総括としてその仕様を満たす AsyncIterator クラスを自作してみました。

class AsyncIterator {
  constructor() {
    this.sec = 0
  }
  next() {
    const value = this.sec++;
    return delay(1000).then(() => ({ value, done: false }))
  }
}

function delay(msec: number) {
  return new Promise(resolve => setTimeout(resolve, msec))
}

const gen = new AsyncIterator()

;(async () => {
  const iterable = { [Symbol.asyncIterator]: () => gen }
  for await (const value of iterable) {
    console.log(await value)
  }
})()

これで今まで通りの結果を出力します。

初めは Generator を使おうと思ったのですが function*..yeild の動作が yield value{ value, done } として取得するようになっていたため、生成する値に Promise を絡める方法が見つけられませんでした。

なので Async Generator が新たに定義された、ということなのかもしれませんね。

という感じで今回は以上です。

  • Symbol
  • Generator, Iterator, for..of
  • Async Generator, Async Iterator, for..await..of

など盛りだくさんでしたが、それぞれの挙動を具体的に把握できたので今後はいろんなドキュメントを読みやすくなったのではと手応えを感じております。

雑感

Symbol について

Symbol('KEY1') === Symbol('KEY1')false になるのですがまだその理由は今ひとつ理解できておりません。なのでまだまだ Symbol を理解できているとは言えない状態ですが、今回は Async Iterator までたどり着くのが趣旨なので Symbol についてはここらで切り上げます。

@@iterator という表記

@@iterator という表記をよく見かけるのですが、これは Symbol.iterator の省略記法ということで良いのでしょうか、具体的な記述が見つけられないでおります。