言語編 |
Generatorは2015年にDart 1.9から導入されている。基本的な解説は言語仕様担当だったGilad BrachaによるDart Language Asynchrony Support: Phase 2が参考になる。但しその後変更が加えられているので注意が必要である。
ここではLanguage tourに沿ってごく簡単に紹介する。
ジェネレータ関数とは結果のシーケンスを後回しで計算する関数である。ジェネレータには同期と非同期の2種類がある:
同期ジェネレータはオンデマンドで値たちを作り出す—受け手はこのジェネレータから値たちを引き取る(プルする)。具体的には同期ジェネレータはIterableのオブジェクトを返す
非同期のジェネレータは自分自身のペースで値たちを作り出し、それを受け手が見つけ得る場所にプッシュする。具体的には非同期ジェネレータはStreamのオブジェクトを返す。
非同期対応の章で示した表をここに再掲する:
関数の4つの型
|
単一 |
複数 |
同期 |
T |
Iterable<T> |
非同期 |
Future<T> |
Stream<T> |
同期ジェネレータ関数は呼び出されると直ちにIterableオブジェクトを返す。これはasyncとマークされた関数が直ちにfutureを返すのと良く似ている。この関数のボディは該 iterator上でmoveNextメソッドが呼ばれるまで実行されない。yield文は式を有し、その式が計算される。計算が終了したらこの関数が停止し、moveNext はtrueを返す。次回 moveNextが呼ばれたらこの関数は計算を再開する。ループが終了したらこのメソッドは暗示的にreturnを実行し、これでこの関数呼び出しは終了し、moveNextは呼び出し側にfalseを返す。
非同期ジェネレータ関数は呼び出されると直ちにStreamのオブジェクトを返す。これはsync*関数がIterableのオブジェクトを直ちに返すのと似ている。またasyncとマークされた関数が直ちにfutureを返すのとも良く似ている。このストリームをlistenすることで、その関数ボディの実行が始まる。この関数は必ずしも保留状態になるわけではないが、通常の非同期関数は何らかのイベントによってその時点でデータの値を送信する。従って主導権は受けて(consumer)ではなく、該ストリームがリスナ関数にその値を渡す。
同期ジェネレータ関数を実装するには、その関数ボディをsync*とマークし、値たちを渡す為にyield文を使う:
|
非同期ジェネレータ関数を実装するには、その関数ボディをasync*とマークし、値たちを渡す為にyield文を使う:
|
自分のジェネレータが再帰的なものなら、yield*を使って性能を改善できる:
|
ここではもう少し具体的なサンプルを示す。これらのコードはDartPadで確認できる。
次の例は流れを理解するためのものである:
|
ここではmain()のなかのforEachメソッドでIterator.moveNext()
により
この同期関数を呼び出すごとにこのボディが実行される。
main()関数は次のように書いても良い:
|
非同期ジェネレータ関数を使う例を示す:
|
このコードはDartPadで確認できる。コンソール上には例えば次のように表示される:
|
最初にこの非同期ジェネレータ関数が動くために(つまりs1がログに書き込まれるまでに)時間がかかることに注意する。
main()関数は次のように書いても良い:
|
yieldは魅力的ではあるが、再帰的な関数の場合は時間が二乗的にかかってしまうという問題が生じる。次のようなnから1まで逆に計数する同期ジェネレータ関数を考えてみよう:
|
このコードは正しく動作するが二乗的に時間がかかってしまう。結果のシーケンスのn番目の要素を得るのにyield iを(n-1)回実行する。例えば最初の要素の3に対しては yield n;文によってイールドされる2番目の要素2にはyield n;文によって1回とyield n; によって1回イールドされる。3番目の要素の1に対してはyield n;文によって1回とyield n; によって2回イールドされる。即ち全部合わせるとn(n − 1)回のyield i;によるイールドが行われる。
yield* (yield-eachと発音される) 文はこの問題の回避のために用意されている。yield*のあとの式は別のシーケンスであることを示す。yield*は副シーケンスの全要素を現在組み立てられているシーケンスに挿入するもので、あたかも各要素に対して個々のyieldがあるようにする。
|
この場合の実行時間はnに対してリニアにかかるだけである。