※ この記事は 2022年4月 に作成したものを一部改稿したものです。
イテレータ (iterator) とは、プログラミング言語において配列やそれに類似するコレクションの各要素を取り出して繰り返し処理をするための仕組みで、「反復子」と訳されます。
イテレータには、コレクションが自身の各要素に対して繰り返し処理を呼び出す「内部イテレータ」(一般的に「for-each」などの名前で用意されることが多い) と、コレクションの各要素を順に参照するオブジェクトを生成しそれを用いて繰り返し処理を行う「外部イテレータ」がありますが、多くの場合、内部イテレータは内部で外部イテレータ (反復子オブジェクト) を生成して処理を行っています。
自作のオブジェクトで内部イテレータを利用できるようにするには、外部イテレータを生成するメソッドや関数を実装する必要があることが多いです。
Java のイテレータ
まずは、Java を例にイテレータの実装方法について見ていきます。
Java ではバージョン 1.5 から拡張 for 文 (for-each ループ) と呼ばれる構文が導入され、内部イテレータを容易に利用できるようになりました。
あるオブジェクトを拡張 for 文の対象とするためには、java.lang.Iterable インターフェースを実装する必要があります。(配列は例外)
Iterable インターフェースはイテレータを返却する iterator() 抽象メソッドを持ち、実装するにはイテレータの生成処理を作成する必要があります。
よく利用される List や Set などのコレクション (の実装) は Iterable インターフェースを実装しており、拡張 for 文の対象とすることができます。
以下は Java におけるイテレータの実装例です。
class MyContainer implements Iterable<Object> {
private Object[] elements;
public MyContainer(Object... data) {
this.elements = Objects.requireNonNull(data);
}
@Override
public Iterator<Object> iterator() {
return new Iterator<Object>() {
int cursor = 0;
@Override
public boolean hasNext() {
return cursor < elements.length;
}
@Override
public Object next() {
return elements[cursor++];
}
};
}
}
要素を内部の配列に保持する MyContainer クラスを定義し、Iterable インターフェースを実装しています。
iterator() メソッドでは java.util.Iterator インターフェースを実装した匿名クラスをイテレータとして返却しています。
Iterator インターフェースは、次の要素があるかどうかを返却する hasNext() と次の要素を返却する next() という2つの抽象メソッドを持っています。
この例では、配列のどの位置の要素を参照しているかを表すカーソル変数を定義し、hasNext() はカーソルの値と配列の要素数を比較する処理、next() はカーソルを1つ進めてその位置の要素を返却する処理として実装しています。
これにより、MyContainer オブジェクトは拡張 for 文の対象とすることができます。
for (Object o : new MyContainer(1, 'a', true)) {
System.out.println(o);
}
Iterable インターフェースには Java 1.8 から forEach() デフォルトメソッドが追加されており、関数型インターフェースを用いて以下のように書くことも可能です。
new MyContainer(1, 'a', true).forEach(System.out::println);
さらに、分割可能なイテレータであるスプリッテレータを返却する spliterator() デフォルトメソッドも追加されており、Stream を簡単に生成することができます。
Stream<Object> s = StreamSupport.stream(new MyContainer(1, 'a', true).spliterator(), false);
s.reduce((a, b) -> String.format("%s, %s", a, b)).ifPresent(System.out::println); // 1, a, true
JavaScript のイテレータ
ここからは JavaScript でイテレータを実装してみます。
Java と JavaScipt 両方の経験がある方でも、「Java のイテレータは見たり使ったりしたことがあるけど JavaScript のイテレータは見たことがない」という方も多いかもしれません。
JavaScript のイテレータは ECMAScript 2015 (ES6) で登場した比較的新しい機能になります。
ES6で「反復可能 (iterable) プロトコル」という概念が導入され、反復可能なオブジェクトを反復するための for...of という構文が導入されました。
あるオブジェクトが反復可能であるためには、イテレータを返却する @@iterator メソッドを実装している必要があります。
@@iterator メソッドは、[Symbol.iterator] というプロパティで利用することができます。
Symbol は同じくES6で導入された新しいデータ型で、グローバルに一意な値を表すプリミティブ値です。
Symbol 値は、文字列値と同様にオブジェクトのプロパティとして利用することができます。
Symbol.iterator は「Well-known symbols」と呼ばれるあらかじめ定義された定数のうちの1つです。
組み込みオブジェクトのうち、配列 (Array) や文字列 (String) はプロトタイプに [Symbol.iterator] プロパティを持つ反復可能オブジェクトで、for...of 文の対象とすることができます。
for (const v of [1, 'a', true]) {
console.log(v);
}
あるオブジェクトが [Symbol.iterator] プロパティを持つかどうかは、Object.getOwnPropertySymbols() メソッドで確認することができます。
Object.getOwnPropertySymbols(Array.prototype) // [ Symbol(Symbol.iterator), Symbol(Symbol.unscopables) ]
Object.getOwnPropertySymbols(Object.prototype) // []
組み込みオブジェクトの Object は [Symbol.iterator] プロパティを持っておらず、反復可能オブジェクトではありません。
そこで、Object に @@iterator メソッドを実装し、反復可能オブジェクトにしてみたいと思います。
通常の実装
@@iterator メソッドは、next() メソッドを実装したオブジェクトをイテレータとして返却します。
next() メソッドは、以下のような done プロパティと value プロパティを持つオブジェクトを返却する必要があります。
{
done: false,
value: 1
}
done は返却する値がある場合は false, 返却する値がない場合は true を指定します。
value は返却する任意の値です。(done が true の場合は省略可)
まずは、オブジェクト自身が持つ列挙可能なプロパティの値を反復するイテレータを実装してみます。
Object.prototype[Symbol.iterator] = function () {
const values = Object.values(this);
let cursor = 0;
return {
next() {
if (cursor >= values.length)
return { done: true };
return { done: false, value: values[cursor++] };
}
};
};
基本的な考え方は Java の場合と一緒で、カーソルを1つずつ進めてプロパティの値の配列から要素を返却しています。
これにより、オブジェクトを for...of 文の対象とすることができます。
for (const v of { a: 1, b: 'a', c: true }) {
console.log(v); // 1, a, true
}
また、ES6で追加されたスプレッド構文 (...) や Array.from() メソッドを利用すると、反復可能オブジェクトから配列を簡単に生成できます。
[...{ a: 1, b: 'a', c: true }] // [1, 'a', true]
Array.from({ a: 1, b: 'a', c: true }) // [1, 'a', true]
ジェネレータを利用した実装
前項では next() メソッドを自分で実装しましたが、同じく ES6 で追加された ジェネレータ を利用すると、もっと簡単にイテレータを実装することができます。
ジェネレータを利用するためには、ジェネレータ関数を function* 宣言を用いて定義します。
ジェネレータ関数を実行するとジェネレータオブジェクト (Generator) が返却されます。
ジェネレータオブジェクトは next() メソッドを持つイテレータの一種で、next() メソッドを呼び出すたびにジェネレータ関数内で yield 演算子に与えられた値を順に返却します。
function* gf() {
yield 1;
yield 'a';
yield true;
}
const g = gf();
g.next(); // { value: 1, done: false }
g.next(); // { value: 'a', done: false }
g.next(); // { value: true, done: false }
g.next(); // { value: undefined, done: true }
ジェネレータ関数を @@iterator メソッドに指定することで、前項のイテレータは以下のように簡単に記述することができます。
Object.prototype[Symbol.iterator] = function* () {
for (const v of Object.values(this)) {
yield v;
}
};
さらに、与えられた反復可能オブジェクトの値を順に返却する yield* 演算子を用いれば、もっと簡単に記述することができます。
Object.prototype[Symbol.iterator] = function* () {
yield* Object.values(this);
};
特定のオブジェクトにのみ実装
これまでは Object のプロトタイプにイテレータを実装していたため、生成する全てのオブジェクトに影響がありましたが、特定のオブジェクトのみ反復可能オブジェクトとすることも可能です。
const o = {
*[Symbol.iterator]() { // [Symbol.iterator]: function* () { と書くのと同じ
for (let i = 0; i < 10; i++) {
yield i;
}
}
};
[...o] // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[...{}] // Uncaught TypeError: {} is not iterable
その他の組み込みイテレータ
Array.prototype.keys() メソッドは、配列内の要素のキー (添え字) を反復する Array Iterator オブジェクトを返します。
これを利用すると、連番を要素に持つ配列を簡単に生成できます。
[...Array(10).keys()] // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
終わりに
イテレータは、コレクションの要素を反復する以外にも、開始値と終了値が決まっている連続する値範囲などを扱う際にも大変便利な仕組みです。
今回は私が使い慣れている Java と JavaScript で実装してみましたが、他の言語の反復処理についても知識を深めていきたいです。
主な参考文献
- Iterable (Java SE 17 & JDK 17)
https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/lang/Iterable.html - Iterator (Java SE 17 & JDK 17)
https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/util/Iterator.html - 反復処理プロトコル - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Iteration_protocols - Symbol.iterator - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator - イテレーターとジェネレーター - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Iterators_and_Generators