Iteratorに対してmapやfilterのようなヘルパー関数を作用させる
Baseline 2025でIterator Helpersが導入され、Iteratorオブジェクトにmapやfilterなどの便利な操作を作用できるようになりました。これにより、配列への変換が不要になり、コードの可読性向上と遅延評価によるパフォーマンス改善が期待できます。
はじめに
Baseline 2025にIterator Helpersが追加されました。
Loading...
Loading...
github.com
具体的には、Iteratorオブジェクトに対して、map・filter・take・drop・flatMap・reduce・toArray・forEach・some・every・findという操作と静的メソッドfromの利用が可能になりました。
Iteratorオブジェクト
Iteratorオブジェクトは繰り返し可能なデータの並びを処理するオブジェクトです。
nextという操作で次のステップで進み、戻り値は次のデータを表すvalueと後続のデータの有無を示すdoneのオブジェクトです。
Iteratorオブジェクトは、他の多くの組み込みイテレータの基盤となる存在です。
Array.prototype[Symbol.iterator]やMap.prototype.values、Set.prototype.values、Intl.Segmenter.prototype.segment等の結果やNodeListなど、さまざまな反復処理はIteratorオブジェクトを継承しています。
const iterator = [1, 2, 3][Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
for (const str of 'hello'[Symbol.iterator]()) {
console.log(str); // 'h' 'e' 'l' 'l' 'o'
}
Iterator Helpers
これまでIteratorオブジェクトに対して何らかの処理を行いたい場合、for...ofループを使って自前で煩雑な処理を記述したり、
配列に変換してから配列に実装されているメソッド(mapやfilterなど)を利用していました。
const set = new Set([1, 2, 3]);
console.log([...set.values()].some((i) => i % 2 === 0)); // true
const segmenter = new Intl.Segmenter('ja', {
granularity: 'grapheme',
});
[...segmenter.segment('あいうえお')].forEach((x) =>
console.log(x.segment),
); // 'あ' 'い' 'う' 'え' 'お'
const nodeList = document.querySelectorAll('div');
Array.from(nodeList).forEach((x) => console.log(x.innerText)); // '...', '...'
上記のコードでは[...set.values()]のようにIteratorオブジェクトを配列に変換するような中間処理が挟まれるので非効率です。
また、配列に変換するタイミングで全てのデータを取り出すので、Iteratorオブジェクトが持つ呼び出されたタイミングにデータを取り出すという性質を生かせません。
Baseline 2025で追加されたIteratorオブジェクトに対する便利な操作(Iterator Helpers)を使えば、これらの問題を解決できます。
const set = new Set([1, 2, 3]);
console.log(set.values().some((i) => i % 2 === 0)); // true
const segmenter = new Intl.Segmenter('ja', {
granularity: 'grapheme',
});
segmenter
.segment('あいうえお')
[Symbol.iterator]()
.forEach((x) => console.log(x.segment)); // 'あ' 'い' 'う' 'え' 'お'
const nodeList = document.querySelectorAll('div');
nodeList[Symbol.iterator]().forEach((x) => console.log(x.innerText)); // '...', '...'
コード上で宣言的で読みやすい([Symbol.iterator]()は見慣れないかもですが)ことに加えて、Iteratorオブジェクトのまま扱えるのでsome等の処理が遅延評価されるようになります(forEachはされません)。
これにより、必要な分のデータだけが評価されるので、パフォーマンスの面でも有利です。
使い方
nextメソッドを実行すると0から3までの値を返すIteratorオブジェクトのような値counterについて考えます。
const counter = {
count: 0,
next() {
if (this.count < 3) {
return { value: this.count++, done: false };
}
return { value: undefined, done: true };
},
};
counter.next()を実行するとvalueが2になるまで、doneがfalseのオブジェクトを返します。
console.log(counter.next()); // { value: 0, done: false }
console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: undefined, done: true }
from
fromは、Iteratorオブジェクトの静的メソッドです。引数に渡した反復可能なオブジェクトから新しいIteratorオブジェクトを生成します。
これにより[Symbol.iterator]()を経由してnextメソッドを呼び出せるようになります。
const counterIter = Iterator.from(counter);
console.log(counterIter[Symbol.iterator]().next()); // { value: 0, done: false }
console.log(counterIter[Symbol.iterator]().next()); // { value: 1, done: false }
console.log(counterIter[Symbol.iterator]().next()); // { value: 2, done: false }
console.log(counterIter[Symbol.iterator]().next()); // { value: undefined, done: true }
以後紹介するメソッドはこの変換を通すことで利用可能になります(Set.prototype.valuesのようなIteratorオブジェクトを継承したオブジェクトで実行する場合は変換不要です)。
map
mapは、Iteratorオブジェクトの各要素に対して指定した関数を適用し、新しいIteratorオブジェクトを返します。
const counterIter = Iterator.from(counter);
const mappedIter = counterIter.map((x) => x * 2);
console.log(mappedIter.next()); // { value: 0, done: false }
console.log(mappedIter.next()); // { value: 2, done: false }
console.log(mappedIter.next()); // { value: 4, done: false }
console.log(mappedIter.next()); // { value: undefined, done: true }
mapを実行したタイミングではなく、next()を呼び出すタイミングで指定した関数の計算が行われています。
本来map中の副作用は避けるべきですが、遅延評価をわかりやすくするためにconsole.logを入れると、評価されるタイミングがわかります。
const counterIter = Iterator.from(counter);
const mappedIter = counterIter.map((x) => {
console.log(x);
return x * 2;
});
console.log(mappedIter.next()); // 0 { value: 0, done: false }
console.log(mappedIter.next()); // 1 { value: 2, done: false }
console.log(mappedIter.next()); // 2 { value: 4, done: false }
mapのコールバック関数中のconsole.logがnextを呼び出したタイミングで呼び出されていることがわかります。
今後紹介する操作のほとんどはこのように遅延評価が行われます。遅延評価の仕組みが分かりにくい関数や遅延評価しない関数についてのみこの仕様について言及します。
filter
filterは、Iteratorオブジェクトの各要素に対して指定した関数を適用し、真となる要素だけを含む新しいIteratorオブジェクトを返します。
const counterIter = Iterator.from(counter);
const filteredIter = counterIter.filter((x) => x % 2 === 0);
console.log(filteredIter.next()); // { value: 0, done: false }
console.log(filteredIter.next()); // { value: 2, done: false }
console.log(filteredIter.next()); // { value: undefined, done: true }
filterも遅延評価されます。偽だった場合は真になるまで値を取り出し続けます。
const counterIter = Iterator.from(counter);
const filteredIter = counterIter.filter((x) => {
console.log(x);
return x % 2 === 0;
});
console.log(filteredIter.next()); // 0 { value: 0, done: false }
console.log(filteredIter.next()); // 1 2 { value: 2, done: false }
console.log(filteredIter.next()); // undefined { value: undefined, done: true }
take
takeは、Iteratorオブジェクトから指定した数の要素を取り出し、新しいIteratorオブジェクトを返します。
const counterIter = Iterator.from(counter);
const takenIter = counterIter.take(2);
console.log(takenIter.next()); // { value: 0, done: false }
console.log(takenIter.next()); // { value: 1, done: false }
console.log(takenIter.next()); // { value: undefined, done: true }
drop
dropは、Iteratorオブジェクトから指定した数の要素をスキップし、新しいIteratorオブジェクトを返します。
const counterIter = Iterator.from(counter);
const droppedIter = counterIter.drop(2);
console.log(droppedIter.next()); // { value: 2, done: false }
console.log(droppedIter.next()); // { value: undefined, done: true }
flatMap
flatMapは、Iteratorオブジェクトの各要素に対して指定した関数を適用し、得られた値をフラットにした新しいIteratorオブジェクトを返します。
const counterIter = Iterator.from(counter);
const flatMappedIter = counterIter.flatMap((x) => [x, x * 2]);
console.log(flatMappedIter.next()); // { value: 0, done: false }
console.log(flatMappedIter.next()); // { value: 0, done: false }
console.log(flatMappedIter.next()); // { value: 1, done: false }
console.log(flatMappedIter.next()); // { value: 2, done: false }
console.log(flatMappedIter.next()); // { value: 2, done: false }
console.log(flatMappedIter.next()); // { value: 4, done: false }
console.log(flatMappedIter.next()); // { value: undefined, done: true }
フラットにしているので、nextで配列がそのままvalueになるのではなく、配列の要素を1つずつnextが呼ばれたタイミングで返します。
reduce
reduceは、Iteratorオブジェクトの各要素に対して指定した関数を適用し、最終的な値を返します。
const counterIter = Iterator.from(counter);
const reducedValue = counterIter.reduce((acc, x) => {
return acc + x;
}, 0);
console.log(reducedValue); // 3
reduceは遅延評価されません。全ての要素を取り出しながら指定した関数を適用します。takeやdropで実行する個数を調整できます。
toArray
toArrayは、Iteratorオブジェクトの全ての要素を配列に変換します。
const counterIter = Iterator.from(counter);
const array = counterIter.toArray();
console.log(array); // [ 0, 1, 2 ]
toArrayも遅延評価されません。全ての要素を取り出しながら配列に変換します。
forEach
forEachは、Iteratorオブジェクトの各要素に対して指定した関数を適用します。
const counterIter = Iterator.from(counter);
counterIter.forEach((x) => {
console.log(x);
});
// 0
// 1
// 2
forEachも遅延評価されません。全ての要素を取り出しながら指定した関数を作用させます。
some
someは、Iteratorオブジェクトの各要素に対して指定した関数を適用し、真となる要素が1つでもあればtrueを返します。
const counterIter = Iterator.from(counter);
const hasOdd = counterIter.some((x) => x % 2 === 0);
console.log(hasOdd); // true
someは遅延評価されます。真となる要素が見つかるまで値を取り出し続け、見つからない場合にfalseを返します。
const counterIter = Iterator.from(counter);
const hasOdd = counterIter.some((x) => {
console.log(x);
return x % 2 === 1;
});
console.log(hasOdd); // 0 1 true
every
everyは、Iteratorオブジェクトの各要素に対して指定した関数を適用し、全ての要素が真であればtrueを返します。
const counterIter = Iterator.from(counter);
const allEven = counterIter.every((x) => x % 2 === 0);
console.log(allEven); // false
everyは遅延評価されます。偽となる要素が見つかるまで値を取り出し続けます。
const counterIter = Iterator.from(counter);
const allEven = counterIter.every((x) => {
console.log(x);
return x % 2 === 0;
});
console.log(allEven); // 0 1 false
find
findは、Iteratorオブジェクトの各要素に対して指定した関数を適用し、真となる要素を返します。
const counterIter = Iterator.from(counter);
const found = counterIter.find((x) => x === 1);
console.log(found); // 1
findは遅延評価されます。真となる要素が見つかるまで値を取り出し続けます。
const counterIter = Iterator.from(counter);
const found = counterIter.find((x) => {
console.log(x);
return x === 1;
});
console.log(found); // 0 1 1
終わりに
Iteratorオブジェクトに追加された便利な操作について紹介しました。
これまでは、for...ofを使って操作を記述したり、配列に変換してから便利な操作を利用したりしていましたが、これからはIterator Helpersによる簡易な記述が可能となりました。
コードがシンプルになり可読性が上がるだけではなく、Iteratorオブジェクトのまま扱えることで遅延評価の恩恵を受けられるのでパフォーマンスの面でも有効です。
これまではIteratorオブジェクトの取り扱いが大変でとりあえず配列に変換していましたが、今後はIterator Helpersを活用して効率的に扱っていきたいです。