前のページ

言語編

次のページ


ジェネレータ関数 (Generators)

Generator2015年に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文を使う:

Iterable<int> naturalsTo(int n) sync* {
  int k = 0;
  while (k < n) yield k++;
}

非同期ジェネレータ関数を実装するには、その関数ボディをasync*とマークし、値たちを渡す為にyield文を使う:

Stream<int> asynchronousNaturalsTo(int n) async* {
  int k = 0;
  while (k < n) yield k++;
}

自分のジェネレータが再帰的なものなら、yield*を使って性能を改善できる:

Iterable<int> naturalsDownFrom(int n) sync* {
  if (n > 0) {
    yield n;
    yield* naturalsDownFrom(n - 1);
  }
}



幾つかのサンプル (Generatoy function samples)

ここではもう少し具体的なサンプルを示す。これらのコードはDartPadで確認できる。



同期ジェネレータ関数の例

次の例は流れを理解するためのものである:

List log = [];
addLog(value) {
  log.add(new DateTime.now().toString().substring(11) + ', value: $value');
}
// sync*関数はIterableを返す
Iterable<int> evenNumbersDownFrom(int n) sync* {
  // このボディはiterator(この例ではforEachメソッドで)がmoveNext()を呼び出すまで実行されない
  while (n >= 0) {
    if (n % 2 == 0) {
      // 'yield' はこの関数を保留にする
      yield n;
    }
    n--;
  }
  // この関数の最後が実行されたら、
  // Iterableの中にはもう値は存在せず、
  // moveNext()は呼び出し側にfalseを返す
}
main() {
  evenNumbersDownFrom(7).forEach(addLog);
  log.forEach(print);
}

ここではmain()のなかのforEachメソッドでIterator.moveNext()によりこの同期関数を呼び出すごとにこのボディが実行される。

main()関数は次のように書いても良い:

main() {
  evenNumbersDownFrom(7).forEach(addLog);
  log.forEach(print);
}



非同期ジェネレータの例

非同期ジェネレータ関数を使う例を示す:

Stream gen(log) async* {
  await for (int k in new Stream.fromIterable([1, 2, 3, 4, 5])) {
    addLog('s' + k.toString());
    if (k <= 5) {
      // そのkに対する必要な処理を非同期で行う
      // 例えば時間がかかるFobonacci関数を計算
      fib(30);
      yield k.toString();
    }
    addLog('f' + k.toString());
  }
}
List log = [];
addLog(value) {
  log.add(new DateTime.now().toString().substring(11) + ', value: $value');
}
printLog() {
  log.forEach(print);
}
// Fibonacci:時間がかかる関数
int fib(int i) {
  if (i < 2) return i;
  return fib(i - 2) + fib(i - 1);
}
main() {
  gen(log).listen((data) {}, onDone: () {
    printLog();
  });
  addLog('reached to the end of the main()');
}

このコードはDartPadで確認できる。コンソール上には例えば次のように表示される:

16:14:08.522, value: reached to the end of the main()
16:14:08.531, value: s1
16:14:08.567, value: f1
16:14:08.569, value: s2
16:14:08.601, value: f2
16:14:08.601, value: s3
16:14:08.636, value: f3
16:14:08.636, value: s4
16:14:08.668, value: f4
16:14:08.668, value: s5
16:14:08.701, value: f5

最初にこの非同期ジェネレータ関数が動くために(つまりs1がログに書き込まれるまでに)時間がかかることに注意する。

main()関数は次のように書いても良い:

main() {
  gen(log).listen((_) {},
      onError: (err) => addLog("An error occured: $err"),
      onDone: () {
        addLog("The stream was closed");
        printLog();
      });
  addLog('reached to the end of the main()');
}



yield*(Yield-each)の例

yieldは魅力的ではあるが、再帰的な関数の場合は時間が二乗的にかかってしまうという問題が生じる。次のようなnから1まで逆に計数する同期ジェネレータ関数を考えてみよう:

Iterable naturalsDownFrom(n) sync* {
  if (n > 0) {
    yield n;
    for (int i in naturalsDownFrom(n - 1)) {
      yield i;
    }
  }
}
main() {
  var it = naturalsDownFrom(3).iterator;
  while (it.moveNext()) {
    print(it.current);
  }
}

このコードは正しく動作するが二乗的に時間がかかってしまう。結果のシーケンスの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があるようにする。

Iterable naturalsDownFrom(n) sync* {
  if ( n > 0) {
    yield n;
    yield* naturalsDownFrom(n-1);
 }
}
main(){
  var it = naturalsDownFrom(7).iterator;
  while (it.moveNext()) {
  print(it.current);
  }
}

この場合の実行時間はnに対してリニアにかかるだけである。





前のページ

次のページ