Generating generators in PHP 5.5

Read this post in English

Share on:

O noua versiune de PHP este pe cale sa se lanseze. In momentul cand scriu acest blog, PHP 5.5 este in beta4.

Dornic de a vedea noutatile, am compilat noua versiune beta. Lista de noutati este disponibila la: http://www.php.net/manual/en/migration55.new-features.php

Cel mai important feature il reprezinta generatoarele (generators).

Generare de generators in PHP 5.5

Un generator este practic o functie care contine un apel catre “yield”.

Sa luam exemplul de pe php.net:

 1<?php
 2function xrange($start, $limit, $step = 1) {
 3    for ($i = $start; $i <= $limit; $i += $step) {
 4        yield $i;
 5    }
 6}
 7
 8echo 'Single digit odd numbers: ';
 9
10/* Note that an array is never created or returned,
11 * which saves memory. */ 
12foreach (xrange(1, 9, 2) as $number) {
13    echo "$number ";
14}
15?>

Practic, generatorul (xrange in acest caz), in loc sa intoarca un array, o sa genereze cate o valoare pentru a fi prelucrata.

Dar stai… oare asta nu era deja posibil pana la aceasta versiune?

Generatori inainte de PHP 5.5

Pana la versiunea de PHP 5.5 aveam deja iteratori:

 1<?php
 2
 3class xrange implements Iterator
 4{
 5    private $position = 0;
 6    private $start;
 7    private $limit;
 8    private $step;
 9
10    public function __construct($start, $limit, $step = 1)
11    {
12        $this->start = $start;
13        $this->limit = $limit;
14        $this->step = $step;
15        $this->position = 0;
16    }
17
18    function rewind()
19    {
20        $this->position = 0;
21    }
22
23    function current()
24    {
25        return $this->start + ($this->position * $this->step);
26    }
27
28    function key()
29    {
30        return $this->position;
31    }
32
33    function next()
34    {
35        ++$this->position;
36    }
37
38    function valid()
39    {
40        return $this->current() <= $this->limit;
41    }
42}
43
44echo 'Single digit odd numbers: ';
45
46/* Note that an array is never created or returned,
47 * which saves memory. */
48foreach (new xrange(2, 9, 2) as $number) {
49    echo "$number ";
50}
51?>

In afara de faptul ca Iteratorul este un obiect cu mai multe proprietati, practic putem atinge acelasi rezultat.

Dar de ce era nevoie de generatori atunci? Simplu! In loc sa folosim ~40 linii de cod, putem folosi pur si simplu 5 ca sa atingem acelasi scop.

Un alt lucru interesant este ca:

1get_class(printer());

va intoarce Generator.

Deci, practic, un generator va intoarce inapoi un obiect de tip Generator, iar acest obiect extinde Iterator.

Diferenta majora, asa cum este si pe site-ul php.net, este ca generatorul nu poate fi resetat, merge intr-o singura directie.

Trimiterea de informatii catre generator

Da, generatorii functioneaza in doua sensuri, doar ca un anume generator este bun doar pentru un singur sens. Daca sintaxa de mai sus este pentru “producerea” de rezultate, sintaxa de mai jos este pentru “consumare” de date.

Sintaxa pentru un generator “consumator” este simpla:

 1<?php
 2function printer() {
 3    $counter = 0;
 4    while(true) {
 5        $counter++;
 6        $value = yield;
 7        echo $value . $counter . PHP_EOL;
 8    }
 9    echo ‘Never executed...' . PHP_EOL;
10}
11
12$printer = printer();
13$printer->send('Hello!');
14echo 'Something is happening over here...' . PHP_EOL;
15$printer->send('Hello!');
16?>

Outputul va fi:

1Hello!1
2Something is happening over here...
3Hello!2

Practic, valoarea din yield poate fi folosita ca orice alta valoare. Ce este interesant este while-ul. Pe php.net este urmatorul comentariu:

// Sends the given value to the
// generator as the result of
// the yield expression and
// resumes execution of the
// generator.

Este nevoie de un loop, pentru ca generatorul se va opri dupa ce proceseaza valoarea si va continua doar atunci cand primeste o noua valoare. Daca scoatem while-ul, doar prima valoare va fi procesata, indiferent de cate ori vom apela send().

Un lucru interesant este ca ceea ce este dupa loop nu se va executa, in cazul meu linia:

1echo ‘Never executed...' . PHP_EOL;

Desi pare un loc potrivit sa eliberezi o resursa (ex. BD sau fisier), de fapt nu este, pentru ca acel cod nu se va executa.

Mi se pare util la logging. Din nou, nimic ce nu putea fi facut si pana acum, dar totusi permite o abordare mult mai usoara.

Am descoperit totusi un lucru care nu functioneaza:

 1<?php
 2function printer() {
 3    while(true) {
 4        echo yield . PHP_EOL;
 5    }
 6}
 7
 8$printer = printer();
 9$printer->send('Hello world!');
10
11foreach($printer as $line) {
12    echo $line . PHP_EOF;
13}

Un pic haotic, nu? Eram curios ce se intampla:
Fatal error: Uncaught exception ‘Exception’ with message ‘Cannot rewind a generator that was already run’ in…

Odata ce a fost folosit send() pe un iterator, nu mai poti sa iterezi prin el. Evident se poate genera unul nou, cu:

1printer();

Ce este si mai confuz este ca Generator este o clasa final, deci nu poate fi extinsa, iar daca incerci sa o instantiezi direct (desi chiar daca ar functiona ar fi inutil):
Catchable fatal error: The “Generator” class is reserved for internal use and cannot be manually instantiated in…

Concluzia

Este un feature interesant, pentru ca simplifica mult lucrurile atunci cand vrei sa construiesti un iterator.

De asemenea, functionalitatea de send() mi se pare foarte interesanta, nu pentru ca face ceva nou, ci pentru ca il face mai usor.

Nu-mi place in schimb ca este aceeasi sintaxa pentru ambele variante de generatori si mai mult, ce este dupa while nu se mai executa.

Mi se pare usor confuza sintaxa, pentru ca nu este o diferentiere clara. Pe de alta parte, se pare ca asta exista deja in Python, deci pentru inspiratie se pot folosi exemplele din acest limbaj.