Developpez.com

Le Club des Développeurs et IT Pro

PHP introduit les générateurs

Par un mécanisme similaire à celui de Python avec le mot-clé yield

Le 2012-09-04 22:05:09, par tarikbenmerar, Chroniqueur Actualités
Les générateurs sont un moyen simple et puissant de créer des itérateurs dans des langages tels que Python. Maintenant, c'est PHP qui fait le pas et s'approprie ce concept.

Pour comprendre l'utilité et la puissance de ce dernier, on revoit l'exemple typique de lecture d'un fichier en entier :
Code :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function getLinesFromFile($fileName) {
    if (!$fileHandle = fopen($fileName, 'r')) {
        return;
    }

    $lines = [];
    while (false !== $line = fgets($fileHandle)) {
        $lines[] = $line;
    }

    fclose($fileHandle);

    return $lines;
}

$lines = getLinesFromFile($fileName);
foreach ($lines as $line) {
    // do something with $line
}
Le point faible de ce code est le fait qu'il copie tout le fichier dans un grand tableau. Ainsi, plus le fichier est grand, plus le besoin en mémoire s'accroît, avec un risque imminent d'atteindre les limites. Il faut toujours se rappeler qu'un script PHP doit respecter une limite de mémoire spécifiée par l'administrateur.

On peut certainement éviter ce comportement et récupérer les données ligne par ligne, en utilisant les itérateurs qui sont parfaits pour ce cas d'utilisation. Malheureusement, en PHP il n'existait jusque-là aucune manière simple d'implémenter les itérateurs. Pour y arriver, on est amené à créer une classe complexe implémentant une interface Iterator comme suit :

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
class LineIterator implements Iterator {
    protected $fileHandle;
    protected $line;
    protected $i;

    public function __construct($fileName) {
        if (!$this->fileHandle = fopen($fileName, 'r')) {
            throw new RuntimeException('Couldn\'t open file "' . $fileName . '"');
        }
    }

    public function rewind() {
        fseek($this->fileHandle, 0);
        $this->line = fgets($this->fileHandle);
        $this->i = 0;
    }

    public function valid() {
        return false !== $this->line;
    }

    public function current() {
        return $this->line;
    }

    public function key() {
        return $this->i;
    }

    public function next() {
        if (false !== $this->line) {
            $this->line = fgets($this->fileHandle);
            $this->i++;
        }
    }
    public function __destruct() {
        fclose($this->fileHandle);
    }
}

$lines = new LineIterator($fileName);
foreach ($lines as $line) {
    // do something with $line
}
Pour un cas aussi simple que notre exemple, le code est déjà complexe. Avec les générateurs, on peut considérablement réduire le nombre de lignes de code de manière directe à partir de la première version du code :

Code :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function getLinesFromFile($fileName) {
    if (!$fileHandle = fopen($fileName, 'r')) {
        return;
    }

    while (false !== $line = fgets($fileHandle)) {
        //c'est ici la différence
        yield $line;
    }

    fclose($fileHandle);
}

$lines = getLinesFromFile($fileName);
foreach ($lines as $line) {
    // do something with $line
}
La toute petite différence réside dans l'utilisation du mot clé yield, qui génère une nouvelle donnée dans l'itération.

En effet, l'instruction $lines = getLinesFromFile($fileName) ne renvoie aucune donnée, c'est simplement un générateur qui implémente l'itérateur qui vient d'être créé.

Après, pendant l'exécution de la boucle foreach ($lines as $line), chaque itération génère les données renvoyées par yield, qui seront stockées dans $line. En fait, cette génération implémente un objet Iterator et des appels à Iterator::next() seront effectués. L'exécution s'arrête jusqu'à la rencontre du prochain yield, qui renvoie la prochaine donnée, et ainsi de suite...

Quelques jours après l'introduction du mot clé Finally, se succèdent donc pour PHP les bonnes nouvelles. Ou les emprunts d’autres langages, diront certains.

Source : détails du mot clé yield dans le site de PHP

Et vous ?

Quelle impression vous laisse cette annonce ?
Pouvez-vous trouver d'autres cas d'utilisation intéressants ?
Quelle autre approche de simplification des itérateurs auriez-vous préférée pour PHP ?
  Discussion forum
17 commentaires
  • transgohan
    Expert éminent
    Je trouve qu'avec l'exemple donnée on perd en lisibilité et compréhension.
    Aucun retour de fonction mais il y en a bel et bien un de par l'implication de yield.
    Ils viennent de créer un second mot clé de retour de fonction mais que je ne trouve pas très pratique.

    J'ai parcouru la rfc et j'ai trouvé d'autres exemples qui me chagrinent comme celui là :
    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function echoLogger() {
        while (true) {
            echo 'Log: ' . yield . "\n";
        }
    }
     
    function fileLogger($fileName) {
        $fileHandle = fopen($fileName, 'a');
        while (true) {
            fwrite($fileHandle, yield . "\n");
        }
    }
     
    $logger = echoLogger();
    // or
    $logger = fileLogger(__DIR__ . '/log');
     
    $logger->send('Foo');
    $logger->send('Bar');
    En gros on balance des objets en exécution parallèle avec ce yield et c'est loin d'être un comportement/code qu'on a l'habitude d'utiliser avec ce langage.

    Voilà pour mes deux sous de réflexions.
  • spidermario
    Membre éprouvé
    Envoyé par tarikbenmerar 
    Quelle autre approche de simplification des itérateurs auriez-vous préférée pour PHP ?

    En fait, pour l’exemple donné, une autre était déjà possible :
    Code php :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function forEachLineFromFile($fileName, $f) { 
        if (!$fileHandle = fopen($fileName, 'r')) { 
            return; 
        } 
      
        while (false !== $line = fgets($fileHandle)) { 
            $f($line); 
        } 
      
        fclose($fileHandle); 
    } 
      
    forEachLineFromFile($filename, function($line) { 
        // do something with $line 
    });

    C’est proche de l’approche employée, par exemple, par Scheme et sa fonction call-with-input-file.
  • camus3
    Membre éprouvé
    C'est bien mais on aimerai aussi que de vieux "bugs" soient corrigés , hein

    http://phpsadness.com/

    mais toute nouveauté est bonne à prendre.
  • pcdwarf
    Membre éprouvé
    Je m'étonne du code impératif donné en exemple avec pour argument principal que ça fait charger tout le fichier en ram et que c'est moche.
    En effet Il est inutile et même néfaste de charger tout le fichier en ram si on peut le traiter ligne à ligne.

    Sauf qu'il n'y a pas besoin d'usine à gaz pour traiter un fichier en une seule passe.

    L'exemple deviens

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function dosomethingwhithFile($fileName, $linefunction) {
        if (!$fileHandle = fopen($fileName, 'r')) {
            return false;
        }
     
        while (false !== $line = fgets($fileHandle)) {
            $linefunction($line);
        }
     
        fclose($fileHandle);
        return true;
    }
    La mémoire libérée sera bien mieux utilisée par le système d'exploitation qui est sensé déjà gérer les problématiques de cache/readahead donc ça ne devrait pas être moins performant qu'en lisant tout le fichier d'un coup.

    Au cas ou ça ne serait pas clair, je ne critique pas le nouveau "yield" mais simplement que le problème soulevé dans l'exemple et prétendument résolu par ce nouvel élément relève largement plus d'une mauvaise approche que d'une limitation du langage.
  • elderion
    Membre régulier
    Par rapport a la syntaxe et sa lisibilité :
    si je fais une analogie avec un autre langage qui utilise yield :
    C# utilise bien "yield return" et pas juste "yield".
    J'aurai trouvé plus commode que PHP s'inspire de la syntaxe C# :
    de considérer yield comme un complément du return et pas comme un remplacant.
  • elderion
    Membre régulier
    (ceci reste mon strict point de vue)

    j'ai fait quelques pas vers Python et meme Ruby :
    sans meme parler de puissance et tout ca
    rien que la syntaxe du langage me rebute, ca ressemble a du "vieux code".
    Avec des "def", des __INIT__, puis l'encapsulation optionnelle.
    C'est pas pour troller, juste expliquer pourquoi je suis réfractaire a Python.

    C'est sans doute une question d'affinité, car j'aime la syntaxe Java ou PHP avec des accolades et des points virgule, etc...
    Sans ca j'aurais surement adopté Python ou Ruby.
  • CyberDenix
    Membre à l'essai
    Je vais faire mon captain obvious, mais...

    Code php :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
      
    while (($line = fgets($fileHandle)) !== false) { 
      
      // Il suffit de traiter la ligne directement ici 
      // -> Consommation mémoire quasi constante 
      // -> Pas besoin d'un yield avec post processing super lent 
      
      doSomething($line); 
      
    }

    Si c'est juste pour traiter le fichier ligne par ligne, les itérateurs ou le yield me semblent hors de propos.

    Mais je loupe peut-être le sens caché de cette formidable avancée technologique ?



    Edit : Grilled by spidermario et pcdwarf. J'me sens moins seul !
  • Lupus Michaelis
    Membre du Club
    J'avais été enchanté de découvrir les générateurs avec Python. Mais c'est avec de la pratique qu'on peut vraiment juger de la pertinence d'une telle fonctionnalité. Donc je vais attendre de pester contre les bogues (et trous de sécurité) qu'aura introduit ce nouveau gadget.
  • _skip
    Expert éminent
    Ce n'est pas mal à voir comme ça.

    Mais déléguer un code de lecture de fichiers à une autre méthode, ça impliquerait presque une gestion d'erreur par exception. Si fgets retourne false, ça ne veut pas dire que la fin du fichier a été atteinte, ça peut être une erreur IO.
  • Grimly_old
    Membre averti
    Ce mot clé est un raccourci pour cette structure :

    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
    public class Resultat extends Exception {
        var $resultat;
        public function __constructor($resultat) {
            $this->resultat = $resultat;
        }
        
        public function getResultat() {
            return $this->resultat;
        }
    }
    
    function monIterateur() {
         $resultat = calculs();
         throw new Resultat($resultat);
    }
    
    function utilisation() {
         do {
             try {
                   monIterateur();
             } catch (Resultat $r) {
                   $line = $r.getResultat();
                   $continue = $line === FALSE; //On suppose que nos calculs nous retournent FALSE à la fin de lecture.
                   if ($continue !== FALSE) {
                        doStuff($line);
                   }
             }
         } while ($continue);
         
    }
    C'est un mécanisme identique qui se limite à un seul niveau de profondeur alors que dans mon exemple j'aurais pu utiliser autant d'intermédiaire que je souhaite entre "monIterateur" et "utilisation"

    Je trouve donc ce mot clé comme un sucre syntaxique inutile.