実例とともに学ぶECMAScript 2015 〜Generator〜
hiroqn
Introduction
この記事は最近JavaScriptに入ったGeneratorと呼ばれる機能について知りたい、もっと詳しく知りたいという方をターゲットとしています。
今回はECMAScript 2015に入った機能のgenerator
について解説していきたいと思います。
generator
はNode界隈では2014年ごろから非同期処理を同期的に書くことができるツールとしてよく使われていました。
最近ではasync/await
をよく耳にすることがあると思いますが、async/await
はgenerator
を使ったものに変換可能なので挙動としては近いものと考えてもらって大丈夫です。
What is generator
前置き
では、generator
とはなんでしょうか?「実はgenerator
はIterator
を返す関数なのです!」これが一番説明しやすいので、これをベースに今回は解説していきたいと思います。
というわけでまずIterator
の説明をまだしていないので今からします。
Iterator
Iterator
はよく反復可能オブジェクトと訳されます。少なくともMDNはそうなっていました。
反復可能オブジェクト これは直感的じゃない表現かも知れません。比較的分かりやすい言葉で言うとfor文などの繰り返しを扱うもの、という表現がいいかもしれません。
もし「IteartorとはどんなObjectか」と質問されたら、以下のように答えます。
next
メソッドが存在し、そのメソッドの返り値はvalue
プロパティとdone
プロパティを少なくとも持ったオブジェクトです。value
は反復した結果、done
はこれ以上反復できないかを表す真偽値です。
箇条書きで書くとすると、
Iteratorとは、以下の条件をみたすオブジェクトのことです。
next
メソッドを持っています。next
はvalue
,done
プロパティを持つオブジェクトを返します。valueは反復処理における結果、doneは反復が終了したかです。
型を知りたいという方は、TypeScriptの型定義ファイルを参照してみましょう。
interface IteratorResult<T> {
done: boolean;
value?: T;
}
interface Iterator<T> {
next(value?: any): IteratorResult<T>;
return?(value?: any): IteratorResult<T>;
throw?(e?: any): IteratorResult<T>;
}
となっています。
型を見てもらうとnext
に値が渡せることに気づかれる方もいると思いますが、今回はその機能やreturn
、throw
は使いません。
example
試しにコードを書いてみましょう。
/**
* @param n {number}
* @return {Iterator<number>}
*/
function range(n) {
var value = 0;
return {
next() {
if (value < n) {
const result = {value, done: false};
value += 1;
return result;
}
return {value: undefined, done: true};
}
};
}
実行例
const iter = range(10);
console.log(iter.next());// {value: 0, done: false}
console.log(iter.next());// {value: 1, done: false }
console.log(iter.next());// {value: 2, done: false}
このようにnext
を呼ぶと次の値が来る状態が作れました。どうでしょうか、反復という感じがするでしょうか?
Iterable
Iterable
というものがあります。今までIterator
について説明してきたのもIterable
を説明するためです。generator
についての解説はもう少し後になります。
Iterable
はあるオブジェクトのSymbol.iterator
プロパティにIterator
を返す関数が設定されているものを指します。Symbol
はECMAScript 2015の機能の1つで、string
以外をプロパティに持つことができるようになる機能です。一応TypeScriptの型定義ファイルを参照してみましょう。
interface Iterable<T> {
[Symbol.iterator](): Iterator<T>;
}
つまり、Iterable
は繰り返し処理をすることができるという機能が実装されているオブジェクトということです。この機能は何のために実装されているのでしょうか? 実は、IterableはECMAScript 2015のさまざまな機能において実装に用いられています。
for of
Iterable
なものはfor文のように中身を1つ1つ取り出しながら処理を書くことができます。
for(const i of range(10)) {
console.log(i)
}
実行してもらうと分かりますがこれはエラーになります。なぜならrange
の返すものはIterator
であって、Iterable
ではないからです。というわけで、Iterable
なIterator
を返すようにrange
を再実装してみましょう。
/**
* @param n {number}
* @return {Iterator<number>}
*/
function range(n) {
var value = 0;
return {
next() {
if (value < n) {
const result = {value, done: false};
value += 1;
return result;
}
return {value: undefined, done: true};
},
[Symbol.iterator]() {
return this;
}
};
}
Iterator
自身を返してあげればいいので簡単ですね。上のfor ofの例を実行すると0〜9が出力されると思います。
Array.from
Iterable
は複数の値を扱うので複数の値をArray
にすることができます。
console.log(Array.from(range(10)))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
が表示されると思います。
spread operator
展開演算子と呼ばれるものがあります。これはIterable
なものを展開することができます。例を見ていただいたほうが分かりやすいかと思います。
const array = [...range(5)];
これは[0, 1, 2, 3, 4]
となりArray.from
と同じように感じますがもう少し便利です。
const array = [...range(5), ...range(5), ...range(5)];
// [0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4]
const array = [666, ...range(5), 666];
// [666, 0, 1, 2, 3, 4, 666]
[]
このリテラル中ではArray
になりますが、関数適用の()
の中ではまた別の挙動を見せます。
function f(a, b, c, d, f) {
console.log(a, b, c, d, f)
}
を定義したあとf(...range(5))
を実行してみてください、a,b,c,d,f
に0,1,2,3,4
が入っていると思います。
rest
今度は分割代入と呼ばれる機能の一部として、...
を使うことができます。分割代入は他にも機能がありますが、今回は...
を使うところを紹介します。
const [a, b, c] = range(5); //aに0,bに1,cに2が入ります。
分割代入はこのようにIterable
なものの中身を取り出し変数に代入することができます。
const [x, y, ...z] = range(5);
この時どういう結果になるでしょうか? 実行してもらうとx
に0
,y
に1
,z
に[2,3,4]
のArray
が入っているのが確認できると思います。
また
function g(x, y, ...z) {}
という関数があった場合、g(...range(5))
とすると同様にx,y,z
に上と同じ値が入ります。これはarguments
がIterable
であることとも関連しています。
built in Iterable
String
、Array
、TypedArray
、Map
、Set
や先ほどあったarguments
などはすでにIterable
です。また、後述するgenerator
で作ったIterator
はIterable
になっています。
なので以下の様なコードは同様に動きます。
var [i, j, k] = [1, 2, 3];
[i, j, k] = 'abc'; // i,j,kはa, b, c
const iterator = 'xyz!'[Symbol.iterator]();
iterator.next();//{value: 'x', done: false}
Generator
ようやくgenerator
です。Iterator
をもっと簡単に作りたい気持ちになってきましたか? 先ほどのrange
はgenerator
を使うと、こう書けるようになります。
function *
function * range(n) {
let i = 0;
while (i < n) {
yield i;
i += 1;
}
}
generator
を書くときはfunction * () {}
を使います。このように書くとyield
というキーワードがそのスコープ内で使えるようになり、function * () {}
で書いたgenerator
関数は常にIterator
を返します。そしてyield xxx
のxxx
がIterator
のvalue
になります。なので、今回書いたrange
も今までのrange
とほとんど同様に動く関数になります。
generator
は慣れることが大切と思いますので、今回は詳細な挙動を確認することはしません。
yield*
yield
ではなくyield*
というキーワードがあります。これはIterable
をgenerator
の中で展開することができるものです。つまり yield* xxx
のxxx
には必ずIterable
が来ないといけません。
おそらくこれでは挙動が分かりづらいと思うので、例を上げていきます。
先ほどのrange
の範囲の値を指定した回数繰り返すIterator
を返すような関数があるとします。関数名はrepeatRange(n, c)
にしましょう。rangeRepeat(5, 3)
とすると0〜4が3回繰り返されるようなIterator
が得られるという仕様です。
function * rangeRepeat(n, c) {
while (c > 0) {
yield* range(n);
c -= 1;
}
}
こうです。意外と簡単ですね。
またこの機能を使うと幾つかの関数をgenerator
を使ったまま再帰で書くことができます。
function range(n) {
return function * f(i) {
if (i < n) {
yield i;
yield* f(i + 1)
}
}(0);
}
今回のケースだとそこまで再帰で書くメリットは薄いですが、非常に効果的に書くことができる時もあります。
tips
function * f(n) {
let i = 0;
function g() {
yield 4;
}
}
この例はエラーとなります。g
のなかのスコープではyield
を使うことができないからです。
finish!
「実はgenerator
はIterator
を返す関数なのです!」
どうでしょうか? この言葉の意味がぼんやりながらも分かってもらえると幸いです。
最後に練習問題を用意しましたので、ぜひチャレンジしてみてください。
問題
1 take
Iteratorとnumberを引数に取り、指定された数以下の長さのIteratorを返す関数takeを実装してください。ここでいう長さとはnext
を読んだときの返り値のdone
がtrue
を返すまでの回数です。
ヒント1)take
自体をgenerator
で書くと楽です。また、for~ofも便利です。
テストケース
Array.from(take(range(1000), 5)); // -> [0, 1, 2, 3, 4]
Array.from(take(range(3), 5)); // -> [0, 1, 2]
2 fib
フィボナッチ数列を返すようなIterator
を返す関数fib
をgenerator
で作ってみてください。
テストケース
Array.from(take(fib(), 20));
// -> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
3 split word
"Hello World Welcome To Generator"
のような空白で区切られた文字列を引数に取り、word(空文字列を除く)を1つずつもらえるIterator
を返す関数splitWord
を作ってみてください。
この問題ではyield*
を使うと短く書くことができます。
テストケース
Array.from(splitWord("Hello World Welcome To Generator"))
// -> ["Hello", "World", "Welcome", "To", "Generator"]
脚注
↑1 | take 自体をgenerator で書くと楽です。また、for~ofも便利です。 |
---|