[JavaScript] 繰り返しを操る、Iterator
はじめに
実務でイテレータを触ることがあったので、調べてまとめてみたものです。
間違いがありましたらコメントいただけると嬉しいです🫶
さて、JavaScript を触っているとたまに見ます、Iterator とか Iterable の文字。
for や while のことを「イテレータ」と呼ぶと昔学んだ記憶があるので、なんとなく反復するものだろうという程度の認識で使っていました。しかしよくよく調べるとけっこう便利そうな機能がでてきたので、年度の納めに学んでみようと思った次第です。
イテレータ(Iterator) とは?
基本的には、以下の条件をすべておさえるオブジェクトを指します。
-
next()
メソッドが定義されている -
next()
の戻り値はIterableResult
であること-
IterableResult
は{ value: value, done: done }
の形式のオブジェクトだよ -
value
にはイテレータの次の値、done
には最後まで値を掘り出し終わったかの真偽値を入れるよ
-
ぱっとみ難しく見えますね。ここで最もわかりやすい例として配列を扱ってみます。
以下のような配列を用意しました。
const a = ["たぬき", "きつね", "いたち"]
この配列に対して [Symbol.iterator]()
メソッドを呼び出すことで、イテレータを生成することができます。
const a = ["たぬき", "きつね", "いたち"]
const iter = a[Symbol.iterator]() // Object [Array Iterator] {}
配列からイテレータが生成できる理由は後で説明するとして。
このイテレータに対して、試しに next()
メソッドを呼び出してみます。正しくイテレータが生成されていれば、前述の条件のように IterableResult
が返るはずですね。
const a = ["たぬき", "きつね", "いたち"]
const iter = a[Symbol.iterator]()
console.log(iter.next())
// { value: 'たぬき', done: false }
value
、done
を持った IterableResult
が返りました。初めて呼び出したので、中身は配列の先頭にあった「たぬき」ですね。どうやら正しくイテレータが作成できていそうです。
続けて、すべての要素を取り出してみます。
const a = ["たぬき", "きつね", "いたち"]
const iter = a[Symbol.iterator]()
while (true) {
const result = iter.next()
console.log(result)
// すべて取り出したら止める
if(result.done) break;
}
// { value: 'たぬき', done: false }
// { value: 'きつね', done: false }
// { value: 'いたち', done: false }
// { value: undefined, done: true }
配列の最後まで値を取り出した後、done
が true
になっているのがわかります。
このように、next()
メソッドを実行したときに繰り返しの次の値がいつでも取り出せるのがイテレータです。
イテラブル(Iterable) とは?
同じような単語ですが、イテラブル(Iterable) というのもあります。
こちらは「反復可能」を示すもので、以下の条件を全ておさえるオブジェクトを指します。
-
[Symbol.iterator]()
というメソッドを持つ -
[Symbol.iterator]()
を実行すると、イテレータを返す
イテラブルなオブジェクトは for ~ of
構文でループできたり、スプレッド構文で展開できたりします。
イテラブルなオブジェクト
以下に一例を示します。
-
配列(Array)、マップ(Map)、セット(Set)
配列に限らず、コレクションのほとんどはイテラブルだと思います。 -
文字列(String)
実はイテラブルなオブジェクトです。
試しにfor ~ of
構文に突っ込んでみると、一文字ずつ返してくれます。便利。
for(const s of "吾輩は猫である。名前はたぬき。") {
console.log(s)
}
// 吾
// 輩
// は
// 猫
// で
// ...
- セグメント(Segment)
この子を見ることはあまりありませんが、形態素解析に使用するIntl.Segmenter
のsegment
関数で返されます。
余談ですが、isWordLike
に単語であるかどうかを詰めて返してくれるので、句読点などを弾けたりします。便利。
const str = "吾輩は猫である。名前はたぬき。";
const segmenterJa = new Intl.Segmenter("ja-JP", { granularity: "word" });
const segments = segmenterJa.segment(str);
for(const segment of segments[Symbol.iterator]()) {
console.log(segment);
}
// { segment: '吾輩', index: 0, input: '吾輩は猫である。名前はたぬき。', isWordLike: true }
// { segment: 'は', index: 2, input: '吾輩は猫である。名前はたぬき。', isWordLike: true }
// { segment: '猫', index: 3, input: '吾輩は猫である。名前はたぬき。', isWordLike: true }
// { segment: 'で', index: 4, input: '吾輩は猫である。名前はたぬき。', isWordLike: true }
// { segment: 'ある', index: 5, input: '吾輩は猫である。名前はたぬき。', isWordLike: true }
// { segment: '。', index: 7, input: '吾輩は猫である。名前はたぬき。', isWordLike: false }
// ...
使ってみる
実際にイテレータを活用してみます。
連番を作成する
1, 2, 3, ... のように、連続する値を返すシンプルなイテレータをつくりたいとします。
初めの値(start)と終わりの値(end)、一度のイテレートでどれくらい増えるか(step)を引数にとり、イテレータを返す関数を実装してみます。start
は範囲に含み、end
は範囲に含みません。
(実装は MDN を参考にしました)
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
let nextIndex = start;
let iterationCount = 0;
const rangeIterator = {
next: function () {
let result;
if (nextIndex < end) {
result = { value: nextIndex, done: false };
nextIndex += step;
iterationCount++;
return result;
}
return { value: iterationCount, done: true };
},
};
return rangeIterator;
}
イテレータの利点は、総要素数を気にしなくていいところです。0 ~ Infinity
の要素を持った配列を作成することは(物理性能的に)不可能ですが、イテレータは都度値を生成するため、サイズを気にする必要がなくなりますね。
さて、実際に使用すると以下のような感じになります。
const iter = makeRangeIterator(1, 10)
let result = iter.next();
while (!result.done) {
result = iter.next()
console.log(result)
// すべて取り出したら止める
if(result.done) break;
}
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: 4, done: false }
// { value: 5, done: false }
// { value: 6, done: false }
// { value: 7, done: false }
// { value: 8, done: false }
// { value: 9, done: false }
// { value: 9, done: true }
問題なく実装できました。
ジェネレータ関数(Generator Function)で実装する
さて、実は JavaScript にはより便利なイテレータを生成するための仕組みがあります。これを ジェネレータ関数(Generator Function) と呼びます。
まず、先ほどのコードを見てみます。
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
let nextIndex = start;
let iterationCount = 0;
const rangeIterator = {
next: function () {
let result;
if (nextIndex < end) {
result = { value: nextIndex, done: false };
nextIndex += step;
iterationCount++;
return result;
}
return { value: iterationCount, done: true };
},
};
return rangeIterator;
}
これを、ジェネレータ関数を使って書き換えると以下のようになります。
(実装は MDN を参考にしています)
function* makeRangeIterator(start = 0, end = 100, step = 1) { // ①
let iterationCount = 0;
for (let i = start; i < end; i += step) {
iterationCount++;
yield i; // ②
}
}
すっきりしましたね。順に見ていきます。
①:ジェネレータの定義
ジェネレータ関数は function*
で定義します。
これがついた関数は、実行されたときに中のコードを実行せず、かわりに ジェネレータ(ジェネレータ関数から生まれたイテレータ) を返します。
②:yield で値を返す
ジェネレータのnext()
メソッドを実行すると、はじめてジェネレータ関数の中身が実行されます。
このとき、yield
に注目です。ジェネレータは、いちど実行されると yield
がある行まで処理を行います。そして、yield
で指定された値を返し、一時停止します。次にジェネレータが実行されるとき、最初からではなく、 以前処理を返した yield
の次の行から処理をスタートさせます。
これがジェネレータ関数、およびジェネレータによるイテレータの全貌です。
余談ですが、ジェネレータ関数内でreturn
したら後続するyield
があるかどうかに関わらずdone
がtrue
のIteratorResult
を返しました。
参考
Discussion