Closed43

Iterator Helpersを試す

nanasinanasi
nanasinanasi

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メソッドを使わないのか?
この提案のイテレータを生成するメソッドはすべて遅延型である。 イテレータを消費するのは、イテレータから次のアイテムが必要なときだけです。 特に終わりのないイテレータの場合、これは重要なキーとなる。 どのような形式のイテレータでも汎用的なサポートがなければ、異なるイテレータは異なる方法で扱わなければならない。

nanasinanasi

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

nanasinanasi

まず、Iteratorクラスが公開された
これはどうやら抽象クラスらしく、直接インスタンス化することはできない

new Iterator()
> TypeError: Iterator cannot be constructed directly
nanasinanasi

継承して使うものらしい

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]
nanasinanasi

フィボナッチ数列

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 ]
nanasinanasi

イテレーターは再利用不可
呼び出しごとに結果が変わるので注意

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 ]
nanasinanasi

take
イテレーターから任意の数の要素を取り出したIterator Helper Objectを返す。
これは最初から数えたものみたい。

const iterator = new InfinityIterator()
console.log(iterator.take(3).toArray()) // [0, 1, 2]
nanasinanasi

Iterator Helper Objectというものがあるらしい。

  • Iteratorのインスタンス
  • イテレータープロトコルに則ったオブジェクトを内部に持っている?

Calls the next() method of the underlying iterator, applies the helper method to the result, and returns the result.

nanasinanasi

「イテレータープロトコルに則ったオブジェクト」と「Iteratorのインスタンス」があるからややこしそう

こういうこと?

  • Iterator Object: イテレータープロトコルに則った(=nextメソッドを持った)オブジェクト
  • Iterator Helper Object: Iteratorのインスタンス
nanasinanasi

イテレーターヘルパー(Iteraetor Helper Objectはこう呼ばれることもある)はイテラブル。
[Symbol.iterator]メソッドを持っているのでforofやスプレッド構文が使える

nanasinanasi

toArray
イテレーターを配列に変換する

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

もっと言うと、無限ループのもと

const iterator = new InfinityIterator()
console.log(iterator.toArray()) // 無限ループ
nanasinanasi

スプレッド構文と大体同じ?

const iterator = new InfinityIterator()
console.log([...iterator.take(3)])

ネスト祭りから解放されるなら嬉しい

nanasinanasi

map
マッピングする

const iterator = new InfinityIterator()
console.log(iterator.take(3).map(x => x * 2).toArray()) // [ 0, 2, 4 ]
nanasinanasi

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

nanasinanasi

forEach
全ての要素に対してループする

なお、forEachは遅延評価じゃないので、無限イテレーターで呼び出さないほうがいい

nanasinanasi

drop
任意の数の要素をスキップする

以下は3つの要素(0, 1, 2)をスキップして、その後の3つの要素を配列にした例

const iterator = new InfinityIterator()
console.log(iterator.drop(3).take(3).toArray()) // [3, 4, 5]
nanasinanasi

every
全ての要素が任意の条件をクリアしているかを返す
これは配列のeveryとほぼ同じ

const iterator = new InfinityIterator()
console.log(
  iterator.drop(1) // 1スタートに
  .take(100) // 1 ~ 100
  .every(n => n % 2 !== 101) // true
)
nanasinanasi

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

nanasinanasi

コラッツの問題を延々と試し続けるコード
無限イテレーターと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を使ったほうが速そう(未検証)

nanasinanasi

filter
条件を満たす要素のみにフィルターしたIterator Helper Objectを返す

これが実行された時点では要素は評価されないので、無限イテレーターで使ってもOK

nanasinanasi

find
条件を満たす最初の要素を返す

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

nanasinanasi

コラッツのfind

const numbers = new InfinityIterator().drop(1)
console.log(
  numbers.find(n => {
    collatz(n)
    return false
  })
)
nanasinanasi

ログを取る版

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
  })
)
nanasinanasi

もうちょっと単純な例
1以降の数が順番にあるイテレーターから、2で割り切れる最初の数を返す

const iterator = new InfinityIterator().drop(1)
console.log(iterator.find(n => n % 2 === 0)) // 2
nanasinanasi

flatMap
マッピング関数を受け取り、それを適用した後平坦にしてIterator Helper Objectを返す

ArrayflatMapと大体同じだが、こっちは一時的なコピーが作成されない?らしい

nanasinanasi

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}
nanasinanasi

Mapを合成する

new Map([map1, map2].values().flatMap(x => x))

ArrayflatMap(&flat)は配列のみを平坦にし、同じイテラブルであるMapまでは平坦化しない。
しかしIteratorflatMapは、Iterator<Iterable>のように中にイテラブルがネストされていても平坦化できる。

nanasinanasi

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

nanasinanasi

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

nanasinanasi

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

nanasinanasi

flatMapは戻り値に文字列が来るのを拒否する。
このとき、TypeScriptの型エラーは出ず、実行時エラーとなるので注意。

文字列をイテレーターとして使いたいなら、Iterator.fromが使える。

iterator.take(3).flatMap(x => Iterator.from(String(x * 10))).toArray()
// [ "0", "1", "0", "2", "0" ]
nanasinanasi

reduce
配列のreduceとほぼ同じ。
このメソッドは遅延評価せず、その場で値を評価する。

const iterator = new InfinityIterator().drop(1)
console.log(iterator.take(10).reduce((a, b) => a + b)) // 55
nanasinanasi

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

nanasinanasi

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

nanasinanasi

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は基礎となるイテレーターを直接使用する(データソースを共有する)。
そのため、基礎となっているイテレーターは再利用できない。
イテレーターをフォーク(コピー?)する方法は用意されていない。

nanasinanasi

Iteration protocols

イテレーターやイテラブルに関するプロトコル(仕様?)。
これにはIterator protocolとIterable protocolが含まれる。

反復処理プロトコルは、新しい組み込みオブジェクトや構文ではなくプロトコルです。
これらのプロトコルは以下のような単純な約束事によって、すべてのオブジェクトで実装することができます。

nanasinanasi

Iterator protocol

イテレーターとは何かを定めるプロトコル。

イテレーター(反復子)プロトコル (The iterator protocol) は、値の並び(有限でも無限でも)を生成するための標準的な方法と、すべての値が生成された場合の返値を定義します。

イテレーターは値の並びを生成するためのオブジェクト。

以下の意味で next() メソッドを実装していれば、オブジェクトはイテレーターになります。

これはビルドインのコンストラクタから生成するものではなく(そもそもそんなものはない)、仕様に従ってnextメソッドを実装したオブジェクトをイテレーターと呼ぶ。
また、イテレーターはオプションとしてreturnthrowメソッドを実装できる。

メモ: 特定のオブジェクトがイテレータープロトコルを実装しているかどうかを反射的に(つまり、実際に 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において、iteratoriterator[Symboo.iterator]()を比較するとtruethisが返るため)
  • array[Symbol.iterator]()は呼び出すたびに新しいイテレーターを生成する
nanasinanasi

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(表記合ってる?)である。

このスクラップは2024/11/29にクローズされました