Iterator Helpersを試す

もうSafari以外で使えると聞いて

参考

Why not use Array.from + Array.prototype methods? - proposal
All of the iterator-producing methods in this proposal are lazy. They will only consume the iterator when they need the next item from it. Especially for iterators that never end, this is key. Without generic support for any form of iterator, different iterators have to be handled differently.
機械翻訳
なぜArray.from + Array.prototypeメソッドを使わないのか?
この提案のイテレータを生成するメソッドはすべて遅延型である。 イテレータを消費するのは、イテレータから次のアイテムが必要なときだけです。 特に終わりのないイテレータの場合、これは重要なキーとなる。 どのような形式のイテレータでも汎用的なサポートがなければ、異なるイテレータは異なる方法で扱わなければならない。

Bun v1.1.34でも使えそう!
最新版のVivaldiでもいける(Chromiumだから?)
VSCodeは最新版(1.95.3)にアプデしたら補完が効くようになった

まず、Iterator
クラスが公開された
これはどうやら抽象クラスらしく、直接インスタンス化することはできない
new Iterator()
> TypeError: Iterator cannot be constructed directly

継承して使うものらしい
class InfinityIterator extends Iterator<number> {
private count = 0
next(): IteratorResult<number, undefined> {
return {
done: false,
value: this.count++
}
}
}
const iterator = new InfinityIterator()
const array = iterator.take(3).toArray()
console.log(array) // [0, 1, 2]

もしかしてクロージャから解放された?
これからは継承の時代?

フィボナッチ数列
class Fibonacci extends Iterator<number> {
constructor(
private a: number,
private b: number
) {
super()
}
next(): IteratorResult<number, undefined> {
const value = this.a
this.a = this.b
this.b = value + this.b
return { done: false, value }
}
}
console.log(new Fibonacci(1, 3).take(10).toArray())
// [ 1, 3, 4, 7, 11, 18, 29, 47, 76, 123 ]

イテレーターは再利用不可
呼び出しごとに結果が変わるので注意
const iterator = new InfinityIterator()
console.log(iterator.take(3).toArray()) // [ 0, 1, 2 ]
console.log(iterator.take(3).toArray()) // [ 3, 4, 5 ]
console.log(iterator.take(3).toArray()) // [ 6, 7, 8 ]

Iterator.from
イテラブルorイテレータープロトコルに準拠したオブジェクトをイテレーターに変換する

take
イテレーターから任意の数の要素を取り出したIterator Helper Objectを返す。
これは最初から数えたものみたい。
const iterator = new InfinityIterator()
console.log(iterator.take(3).toArray()) // [0, 1, 2]

Iterator Helper Objectというものがあるらしい。
-
Iterator
のインスタンス - イテレータープロトコルに則ったオブジェクトを内部に持っている?
Calls the next() method of the underlying iterator, applies the helper method to the result, and returns the result.

toArray
イテレーターを配列に変換する
イテレーターの状態だとconsole.log
などの出力で内容が見えない(評価されてないから当たり前)ので、配列に変換すると内容が見れるようになる
つまり、これを呼び出すと全ての要素が評価される
もっと言うと、無限ループのもと
const iterator = new InfinityIterator()
console.log(iterator.toArray()) // 無限ループ

スプレッド構文と大体同じ?
const iterator = new InfinityIterator()
console.log([...iterator.take(3)])
ネスト祭りから解放されるなら嬉しい

map
マッピングする
const iterator = new InfinityIterator()
console.log(iterator.take(3).map(x => x * 2).toArray()) // [ 0, 2, 4 ]

map
はマッピング関数を適用させた新しいIterator Helper Objectを返す
この時点では要素は評価されないので、例えば上のコードのtake
とmap
を逆にしても大丈夫

forEach
全ての要素に対してループする
なお、forEach
は遅延評価じゃないので、無限イテレーターで呼び出さないほうがいい

drop
任意の数の要素をスキップする
以下は3つの要素(0, 1, 2
)をスキップして、その後の3つの要素を配列にした例
const iterator = new InfinityIterator()
console.log(iterator.drop(3).take(3).toArray()) // [3, 4, 5]

every
全ての要素が任意の条件をクリアしているかを返す
これは配列のevery
とほぼ同じ
const iterator = new InfinityIterator()
console.log(
iterator.drop(1) // 1スタートに
.take(100) // 1 ~ 100
.every(n => n % 2 !== 101) // true
)

なお、無限イテレーターでも使うことはできる
その場合、条件を満たし続ける限りevery
の実行は終わらない

コラッツの問題を延々と試し続けるコード
無限イテレーターとevery
で実装してみた
function collatz(number: number) {
while(number !== 1) {
number = number % 2 === 0
? number / 2
: number * 3 + 1
}
}
class InfinityIterator extends Iterator<number> {
private count = 0
next(): IteratorResult<number, undefined> {
return {
done: false,
value: this.count++
}
}
}
const numbers = new InfinityIterator().drop(1)
console.log(
numbers.every(n => {
collatz(n)
return true
})
)
わざわざこんなことをする意味はない
普通にwhile
を使ったほうが速そう(未検証)

↑これ、後述のfind
のほうが適性あるかも

filter
条件を満たす要素のみにフィルターしたIterator Helper Objectを返す
これが実行された時点では要素は評価されないので、無限イテレーターで使ってもOK

find
条件を満たす最初の要素を返す
実行すると要素が評価される
無限イテレーターで使った場合、要素が見つかるまで実行は終わらない

コラッツのfind
版
const numbers = new InfinityIterator().drop(1)
console.log(
numbers.find(n => {
collatz(n)
return false
})
)

ログを取る版
const last = parseInt(await Bun.file('./client/files/collatz.log').text() || '1')
const numbers = new InfinityIterator().drop(last)
console.log(
numbers.find(n => {
collatz(n)
if(n % 200000 === 0) Bun.write(Bun.file('./client/files/collatz.log'), `${n}`)
return false
})
)

もうちょっと単純な例
1以降の数が順番にあるイテレーターから、2で割り切れる最初の数を返す
const iterator = new InfinityIterator().drop(1)
console.log(iterator.find(n => n % 2 === 0)) // 2

flatMap
マッピング関数を受け取り、それを適用した後平坦にしてIterator Helper Objectを返す
Array
のflatMap
と大体同じだが、こっちは一時的なコピーが作成されない?らしい

This avoids creating any temporary copies of the map's content. Note that the array [map1, map2] must first be converted to an iterator (using Array.prototype.values()), because Array.prototype.flatMap() only flattens arrays, not iterables.
new Map([map1, map2].flatMap((x) => x)); // Map(1) {undefined => undefined}

Map
を合成する
new Map([map1, map2].values().flatMap(x => x))
Array
のflatMap
(&flat
)は配列のみを平坦にし、同じイテラブルであるMap
までは平坦化しない。
しかしIterator
のflatMap
は、Iterator<Iterable>
のように中にイテラブルがネストされていても平坦化できる。

flatMap
って、一つの要素につき二つの要素を返す場合があるときに使えるのか
初めて気づいた

Iterator
のflatMap
は、マッピング関数の戻り値がイテレーターかイテラブルでなければならない。
ただマッピングしたいだけならmap
でいい。

flatMap
は要素を評価しないので、無限イテレーターに入れてもすぐに無限ループにはならない。

flatMap
は戻り値に文字列が来るのを拒否する。
このとき、TypeScriptの型エラーは出ず、実行時エラーとなるので注意。
文字列をイテレーターとして使いたいなら、Iterator.from
が使える。
iterator.take(3).flatMap(x => Iterator.from(String(x * 10))).toArray()
// [ "0", "1", "0", "2", "0" ]

reduce
配列のreduce
とほぼ同じ。
このメソッドは遅延評価せず、その場で値を評価する。
const iterator = new InfinityIterator().drop(1)
console.log(iterator.take(10).reduce((a, b) => a + b)) // 55

some
条件を満たす要素が一つでも存在するかどうかのboolean
を返す。
配列のsome
とほぼ同じ。

用語の整理
注訳がない場合の引用元: MDN

Iterator Helper Object
ES2025で公開される予定の、Iterator
クラスのインスタンス。
このクラスにはIterator Helper Methodsと呼ばれるメソッド群がある。
Among the iterator helper methods, filter(), flatMap(), map(), drop(), and take() return a new Iterator Helper object.
The iterator helper is also an Iterator instance, making these helper methods chainable.
Iterator Helper Methodsのうち一部のメソッドは、新しいIterator Helper Objectを返す。
これらのメソッドはチェイン可能なので、例えばiterator.drop(1).take(4)
といった呼び出しができる。
All iterator helper objects inherit from a common prototype object, which implements the iterator protocol:
common prototype object、つまり共通のプロトタイプオブジェクトは、以下のメソッドと持っている?
-
next
: 基礎となるイテレーターのnext
を呼び出し、結果にヘルパーメソッドを適用して返す return
The iterator helper shares the same data source as the underlying iterator, so iterating the iterator helper causes the underlying iterator to be iterated as well.
There is no way to "fork" an iterator to allow it to be iterated multiple times.
Iterator Helper Objectは基礎となるイテレーターを直接使用する(データソースを共有する)。
そのため、基礎となっているイテレーターは再利用できない。
イテレーターをフォーク(コピー?)する方法は用意されていない。

Iteration protocols
イテレーターやイテラブルに関するプロトコル(仕様?)。
これにはIterator protocolとIterable protocolが含まれる。
反復処理プロトコルは、新しい組み込みオブジェクトや構文ではなくプロトコルです。
これらのプロトコルは以下のような単純な約束事によって、すべてのオブジェクトで実装することができます。

Iterator protocol
イテレーターとは何かを定めるプロトコル。
イテレーター(反復子)プロトコル (The iterator protocol) は、値の並び(有限でも無限でも)を生成するための標準的な方法と、すべての値が生成された場合の返値を定義します。
イテレーターは値の並びを生成するためのオブジェクト。
以下の意味で next() メソッドを実装していれば、オブジェクトはイテレーターになります。
これはビルドインのコンストラクタから生成するものではなく(そもそもそんなものはない)、仕様に従ってnext
メソッドを実装したオブジェクトをイテレーターと呼ぶ。
また、イテレーターはオプションとしてreturn
やthrow
メソッドを実装できる。
メモ: 特定のオブジェクトがイテレータープロトコルを実装しているかどうかを反射的に(つまり、実際に next() を呼び出して、返された結果を検証することなく)知ることは不可能です。
イテレーターはとても簡単に反復可能オブジェクトにすることができます。
[@@iterator]()
メソッドを実装して this を返すだけです。// イテレーターと反復可能の両プロトコルを満たす const myIterator = { next() { // ... }, [Symbol.iterator]() { return this; }, };
このようなオブジェクトは反復可能イテレーター(Iterable Iterator)と呼ばれる。
Generator
オブジェクトはiterable Iteratorのひとつ。
したがって、反復可能プロトコルを実装せずにイテレータープロトコルを実装することは、ほとんど有益ではありません。(実際、ほとんどすべての構文と API はイテレーターではなく反復可能を期待しています。)
組み込み反復可能オブジェクトから返されるイテレーターは、実際にはすべて共通のクラス Iterator (現在は未公開)を継承しており、前述の Symbol.iterator { return this; } メソッドを実装しているので、すべて反復可能イテレーターとなっています。
配列が入った変数array
において、
-
array
はIterable -
array[Symbol.iterator]
はIteratorを返すメソッド -
array[Symbol.iterator]()
はIteratorでありIterable -
array[Symbol.iterator]()
が入った変数iterator
において、iterator
とiterator[Symboo.iterator]()
を比較するとtrue
(this
が返るため) -
array[Symbol.iterator]()
は呼び出すたびに新しいイテレーターを生成する

Iterable protocol
Iterable=反復可能とは何かを定めるプロトコル。
反復可能プロトコル (The iterable protocol) によって、 JavaScript のオブジェクトは反復動作を定義またはカスタマイズすることができます
一部の組み込み型は既定の反復動作を持つ組み込み反復可能オブジェクトで、これには Array や Map がありますが、他の型 (Object など) はそうではありません。
反復可能であるために、オブジェクトは@@iteratorメソッドを実装する必要があります。
これはつまり、オブジェクト(または、プロトタイプチェーン上のオブジェクトの一つ)が Symbol.iterator 定数にて利用できる @@iterator キーのプロパティを持つ必要があります。
Symbol.iterator
メソッドを持つオブジェクトは反復可能=Iterableであると言える。
このメソッドはIterator protocolに準拠したオブジェクトを返す必要がある。
これはfor..of
などでオブジェクトが反復されるときに呼び出され、返されたイテレーターはオブジェクトの反復に使われる。
この関数は普通の関数、またはジェネレーター関数にすることができ、そのため呼び出されると、イテレーターオブジェクトが返されます。
ジェネレーター関数はジェネレーターを返す。
ジェネレーターはイテレーターでありイテラブルなので、ジェネレーター関数はSymbol.iterator
に入れることができる。
なお、Symbol.iterator
はWell known symbol(表記合ってる?)である。