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
の振る舞いは
と解釈できますね。
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 Protocol を満たすオブジェクトを返す関数を
- 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
ではループのペイロード value
は Iterator が返す { 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 について
Symbol('KEY1') === Symbol('KEY1')
は false
になるのですがまだその理由は今ひとつ理解できておりません。なのでまだまだ Symbol
を理解できているとは言えない状態ですが、今回は Async Iterator までたどり着くのが趣旨なので Symbol
についてはここらで切り上げます。
@@iterator という表記
@@iterator という表記をよく見かけるのですが、これは Symbol.iterator
の省略記法ということで良いのでしょうか、具体的な記述が見つけられないでおります。