Comprendre sync*, async*, yield et yield* en Dart

post-thumb

Quand on commence à développer une application avec Dart, et qu’on s’intéresse un peu à l’asynchronisme, on voit du code avec le mot clé async , mais c’est quoi async avec l’étoile (*) ? ou encore le sync avec l’étoile : async* et sync*

Tout simplement, ce sont des mots clés utilisés dans des fonctions génératrices ( on parle de generator functions en anglais.

Les generator functions produisent des séquences de valeur ( contrairement aux fonctions régulières , les Regular functions qui retournent une seule valeur)

Les generator functions peuvent être :

  • asynchrones : retournent des valeurs sous forme de Stream
  • synchrones: retournent une liste de valeurs

C’est quoi le Yield ?

Le yield est tout simplement ce mot clé qui permet d’informer qu’on retourne une seule valeur dans la séquence en cours, mais on n’arrête pas la fonction ( la génération des valeurs continue).

Ce qui est très intéressant avec les générateurs , c’est qu’ils produisent les valeurs à la demande, ce qui veut dire que les valeurs de la séquence sont générées lorsqu’on essaie de faire une itération, ou qu’on commence à écouter le stream, PAS AVANT !

Fonctionnement de sync*

Ci-dessous un exemple pour montrer comment fonctionne le sync*

evenNumbersDownFrom
 1main() {
 2  print('starting to iterate...'); 
 3  evenNumbersDownFrom(7).forEach(print);
 4  print('end of main');
 5}
 6
 7// sync* functions return an iterable
 8Iterable<int> evenNumbersDownFrom(int n) sync* {
 9  // the body isn't executed until an iterator invokes moveNext()
10  int k = n;
11  print('generator started');
12  while (k >= 0) {
13    if (k % 2 == 0) {
14      // 'yield' suspends the function
15      yield k;
16    }
17    k--;
18  }
19  print('generator ended');
20  // when the end of the function is executed,
21  // there are no more values in the Iterable, and
22  // moveNext() returns false to the caller
23}

ci-dessous le résultat :

starting to iterate...
generator started
6
4
2
0
generator ended
end of main

Comme on le constate, la fonction génératrice est executée à chaque itération de la fonction forEach.

Fonctionnement de async*

Un autre exemple avec async*

 1import 'dart:async';
 2
 3main() async {
 4  Stream<String> messages = printNumbersDownAsync(5);
 5  print('starting to listen...');
 6  messages.listen(print);
 7  print('end of main');
 8}
 9
10Stream<String> printNumbersDownAsync(int n) async* {
11  int k = n;
12  print('started generating values...');
13  while (k >= 0) {
14    yield await loadMessageForNumber(k--);
15  }
16  print('ended generating values...');
17}
18
19Future<String> loadMessageForNumber(int i) async {
20  await new Future.delayed(new Duration(milliseconds: 50));
21  if (i % 2 == 0) {
22    return '$i is even';
23  } else {
24    return '$i is odd';
25  }
26}

ci-dessous le résultat :

starting to listen...
end of main
started generating values...
5 is odd
4 is even
3 is odd
2 is even
1 is odd
0 is even
ended generating values...

Alors, on a le même résultat qu’en utilisant sync*, mais si on regarde bien le résultat qui est affiché, on constate qu’on est pas obligé d’attendre que la fonction génératrice se termine pour atteindre la fin de la fonction main.

La fonction génératrice n’est pas executée à moins qu’on essaie d’écouter le stream ( la séquence de valeurs) qu’elle génère.

Remplaçons listen par await for

Reprenons la fonction précédente en remplaçant à la ligne 6, la fonction listen par l’approche await for :

 1import 'dart:async';
 2
 3main() async {
 4  Stream<String> messages = printNumbersDownAsync(5);
 5  print('starting to listen...');
 6  await for (String msg in messages) {
 7    print(msg);
 8  }
 9  print('end of main');
10}
11
12Stream<String> printNumbersDownAsync(int n) async* {
13  int k = n;
14  print('started generating values...');
15  while (k >= 0) {
16    yield await loadMessageForNumber(k--);
17  }
18  print('ended generating values...');
19}
20
21Future<String> loadMessageForNumber(int i) async {
22  await new Future.delayed(new Duration(milliseconds: 50));
23  if (i % 2 == 0) {
24    return '$i is even';
25  } else {
26    return '$i is odd';
27  }
28}

ci-dessous le résultat :

starting to listen...
started generating values...
5 is odd
4 is even
3 is odd
2 is even
1 is odd
0 is even
ended generating values...
end of main

En utilisant listen, la fonction n’était pas bloquante , mais avec await for l’itération est bloquante et on constate que la fonction main se termine lorsque la fonction await for a fini son itération.

C’est quoi le Yield* ?

Le mot clé yield* est utilisé pour construire des générateurs récursifs

 1void main() {
 2  print('create iterator');
 3  Iterable<int> numbers = getNumbersRecursive(3);
 4  print('starting to iterate...');
 5  for (int val in numbers) {
 6    print('$val');
 7  }
 8  print('end of main');
 9}
10
11Iterable<int> getNumbersRecursive(int number) sync* {
12  print('generator $number started');
13  if (number > 0) {
14    yield* getNumbersRecursive(number - 1);
15  }
16  yield number;
17  print('generator $number ended');
18}

ci-dessous le résultat :

create iterator
starting to iterate...
generator 3 started
generator 2 started
generator 1 started
generator 0 started
0
generator 0 ended
1
generator 1 ended
2
generator 2 ended
3
generator 3 ended
end of main

Code source complet


Les sources sont disponibles ici.