ECMAScript : Les générateurs, une fausse bonne idée ?

Je parcourais récemment les origines du Javascript pour me perfectionner dans ce langage et comprendre un peu plus ce qu’il s’est passé avant que je ne commence à développer des sites web (la guerre entre netscape, IE et le W3C). C’est alors que je suis tombé sur l’avenir du langage, c’est à dire ECMAScript 6 « Harmony » de nos jours.

J’y ai trouvé ce que je pense être l’une des idées les plus mal introduites dans un langage informatique, les générateurs.

Les générateurs n’ont pas été inventés pour Javascript mais existent déjà en PHP 5.5. Il faut alors apprendre ce pourquoi ils ont été réalisés et comment ils ont été implémentés pour comprendre ce qui a été fait dans ECMAScript 6.

En PHP 5.5 nous pouvons maintenant nous abstenir de l’instruction « return » et utiliser l’instruction « yield ». La fonction retourne alors un générateur et non une valeur.
Un générateur est un objet dont l’idée principale est de fournir une méthode « next » pour accéder à des valeurs successives rendues disponibles par l’instruction « yield ».

Un exemple concret serait l’extraction des différents champs d’une date à partir d’un timestamp. Il existe des fonctions natives pour cela mais imaginons que nous n’y avons pas accès.
On souhaite alors retrouver l’année, le mois, le jour du mois, le jour dans l’année, le jour de la semaine, l’heure, les minutes et secondes … n’importe quelle information commune sur la date. Je vais me restreindre aux données horaires pour diminuer la taille des codes présentés.
Si je me retrouve dans l’incapacité de créer un générateur, en PHP nous aurions du créer cette fonction :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getDateFields($timestamp) {
    $result = array();
    $result['millisecond'] = $timestamp % 1000;
    $timestamp = $timestamp / 1000;
    $result['second'] = $timestamp % 60;
    $timestamp = $timestamp / 60;
    $result['minute'] = $timestamp % 60;
    $timestamp = $timestamp / 60;
    $result['hour'] = $timestamp % 12;
    $result['am/pm'] = $timestamp % 24 >= 12 ? 'pm' : 'am';
    $result['hourOfDay'] = $timestamp % 24;
   
    //...
   
    return $result;
}

Ici nous allouons un tableau de résultats avant de le retourner d’un seul coup. Il y a ici une consommation de mémoire induite par le stockage des résultats intermédiaires. Pourtant nous devrions être capable de les consommer sans placer en cache les différents résultats. C’est l’idée principale des générateurs. Sur des applications plus complexes, ils permettent l’accessibilité de résultats intermédiaires et donc une réduction de la consommation mémoire avec peu d’efforts.

Avec le mot clé yield, le code si dessus deviens le suivant :

1
2
3
4
5
6
7
8
9
10
11
12
13
function getDateFieldsGenerator($timestamp) {
    yield array('millisecond' => $timestamp % 1000);
    $timestamp = $timestamp / 1000;
    yield array('second' => $timestamp % 60);
    $timestamp = $timestamp / 60;
    yield array('minute' => $timestamp % 60);
    $timestamp = $timestamp / 60;
    yield array('hour' => $timestamp % 12);
    yield array('am/pm' => $timestamp % 24 >= 12 ? 'pm' : 'am');
    yield array('hourOfDay' => $timestamp % 24);
   
    //...
}

Le code est très semblable et peux être compris de la même façon. La différence règne dans la consommation mémoire. En effet le moteur PHP ne retourne pas une valeur à l’exécution de cette « fonction » mais un générateur qui n’est autre qu’un itérateur sur les résultats indiqués par le mot clé « yield ». Entre chaque itération, la fonction est reprise au même endroit jusqu’à atteindre le prochain résultat. On arrive aussi à produire une fermeture sur le champ $timestamp car il reste accessible et modifiable avec les itérations successives du générateur. C’est un ajout puissant dans PHP car hormis le contexte d’un objet ou de paramètres, il n’existe pas de fermeture en PHP.

Maintenant il faut comprendre aussi ce qu’un générateur n’est pas fait pour l’itération simple d’une structure quelle qui soit. Par exemple si on souhaite itérer sur un tableau, on préfèrera ce code …

1
2
3
function iterateOver($tab) {
    return new ArrayIterator($tab);
}

… à ce code …

1
2
3
4
5
function iterateOver($tab) {
    foreach ($tab as $value) {
        yeild $value;
    }
}

En javascript, le problème est différent car nous disposons déjà des fermetures et la traduction d’un générateur en code javascript est simple.

Avec ECMAScript 6, deux nouveaux mots clés sont introduits dans le langage, « function* » (l’étoile est importante) et « yield ». Le comportement est similaire avec l’implémentation de PHP aussi pour implémenter le générateur PHP en JS, nous pourrons écrire ceci :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function* getDateFields(time) {
        //.....
        yield { field : 'TIMESTAMP', value : time };
        yield { field : 'ERA', value : time = 12 ? 'pm' : 'am' };
        date = Math.floor(time / oneDay);
        time = posMod(time, oneDay);
        yield { field : 'MILLISECOND', value : time % 1000 };
        time = Math.floor(time / 1000);
        yield { field : 'SECOND', value : time % 60 };
        time = Math.floor(time / 60);
        yield { field : 'MINUTE', value : time % 60 };
        time = Math.floor(time / 60);
        yield { field : 'HOUR', value : time % 12 };
        yield { field : 'HOUR_OF_DAY', value : time % 24 };
        yield { field : 'AM_PM', value : time % 24 >= 12 ? 'pm' : 'am' };
        yield { field : 'DAY_OF_WEEK', value : posMod(date + 4, 7) + 1 };
        //....
}

Cependant ceci est un sucre syntaxique repris de PHP pour en réalité représenter ce code :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
function getDateFields(timestamp) {
      //...

      var iter = Object.create(null), step;
     
      step = 0;
      iter.next = function() {
        var res = null;
        switch(step) {
          case 0:
            res = { field : 'TIMESTAMP', value : time };
            break;
          case 1:
            res = { field : 'ERA', value : time = 12 ? 'pm' : 'am' };
            break;
          case 2:
            date = Math.floor(time / oneDay);
            time = posMod(time, oneDay);
            res = { field : 'MILLISECOND', value : time % 1000 };
            break;
          case 3:
            time = Math.floor(time / 1000);
            res = { field : 'SECOND', value : time % 60 };
            break;
          case 4:
            time = Math.floor(time / 60);
            res = { field : 'MINUTE', value : time % 60 };
            break;
          case 5:
            time = Math.floor(time / 60);
            res = { field : 'HOUR', value : time % 12 };
            break;
          case 6:
            res = { field : 'HOUR_OF_DAY', value : time % 24 };
            break;
          case 7:
            res = { field : 'AM_PM', value : time % 24 >= 12 ? 'pm' : 'am' };
            break;
          case 8:
            res = { field : 'DAY_OF_WEEK', value : posMod(date + 4, 7) + 1 };
            break;
          //....
        }
        if (res === null) {
          return { done : true };
        } else {
          step = step + 1;
          return { done : false, value : res };
        }
      }
      return iter;
}

Le code est tout aussi fonctionnel, clair et certes plus verbeux. Nous n’avons qu’un gain de verbosité (et encore ce n’est qu’un switch+case) à ce qui peux être écrit tout aussi simplement.

Le problème est aussi dans la notation de ce générateur, et c’est un reproche que je fais aussi à l’implémentation PHP, qui est identifié par le mot clé « function* ». Pour un débutant sur le langage, cette notation porte fortement à confusion. Si il veux alors comprendre pleinement la notion des fonctions, il ne doit jamais croiser de générateur ou bien il peux alors se retrouver dans la confusion d’un sucre syntaxique inutile.

ECMAScript 6 donne de bonnes idées avec le retour des itérateurs, les maps et cette nouvelle notion de promesses mais rajoute un sujet de confusion à travers les générateurs.

Laisser un commentaire