Claudiu Persoiu

Blog-ul lui Claudiu Persoiu


Archive for the ‘iterator’ tag

Generating generators in PHP 5.5

without comments

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:

<?php
function xrange($start, $limit, $step = 1) {
    for ($i = $start; $i <= $limit; $i += $step) {
        yield $i;
    }
}

echo 'Single digit odd numbers: ';

/* Note that an array is never created or returned,
 * which saves memory. */ 
foreach (xrange(1, 9, 2) as $number) {
    echo "$number ";
}
?>

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:

<?php

class xrange implements Iterator
{
    private $position = 0;
    private $start;
    private $limit;
    private $step;

    public function __construct($start, $limit, $step = 1)
    {
        $this->start = $start;
        $this->limit = $limit;
        $this->step = $step;
        $this->position = 0;
    }

    function rewind()
    {
        $this->position = 0;
    }

    function current()
    {
        return $this->start + ($this->position * $this->step);
    }

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

    function next()
    {
        ++$this->position;
    }

    function valid()
    {
        return $this->current() <= $this->limit;
    }
}

echo 'Single digit odd numbers: ';

/* Note that an array is never created or returned,
 * which saves memory. */
foreach (new xrange(2, 9, 2) as $number) {
    echo "$number ";
}
?>

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:

get_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:

<?php
function printer() {
    $counter = 0;
    while(true) {
        $counter++;
        $value = yield;
        echo $value . $counter . PHP_EOL;
    }
    echo ‘Never executed...' . PHP_EOL;
}

$printer = printer();
$printer->send('Hello!');
echo 'Something is happening over here...' . PHP_EOL;
$printer->send('Hello!');
?>

Outputul va fi:

Hello!1
Something is happening over here...
Hello!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:

echo ‘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:

<?php
function printer() {
    while(true) {
        echo yield . PHP_EOL;
    }
}

$printer = printer();
$printer->send('Hello world!');

foreach($printer as $line) {
    echo $line . PHP_EOF;
}

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:

printer();

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.

Written by Claudiu Persoiu

10 May 2013 at 9:11 AM

Posted in PHP

Tagged with , , ,

Iterarea obiectelor folosind PHP si SPL

without comments

Iterator pattern este probabil cel mai popular pattern din SPL. Este un exemplu foarte simplu de a demonstra avantajele unei interfete si SPL.

Motivatie:
Posibilitatea de a itera structuri de tip obiect folosind functii precum foreach(), var_dump(), print_r() etc.

Diagrama:

iterator diagram

Structura Iterator:

In SPL se gasesc mai multe interfete si clase pentru iterator.

Structura de baza pentru interfata Iterator:

/**
 * Interfata Iterator din SPL
 */
Iterator extends Traversable {
      /**
       * Intoarce elementul curent
       */
      abstract public mixed current ( void )

      /**
       * Cheia pentru elementul curent
       */
      abstract public scalar key ( void )

      /**
       * Trece la urmatorul element
       */
      abstract public void next ( void )

      /**
       * Reseteaza iterarea la pozitia initiala
       */
      abstract public void rewind ( void )

      /**
       * Verifica daca pozitia curenta este valida
       */
      abstract public boolean valid ( void )
}

Exemplu 1:

Un obiect iterabil simplu.

/**
 * Clasa care va genera un obiect interabil
 */
class Iterabil implements Iterator {

	/**
	 * Index pentru elementul iterabil
	 */
	private $_current = 0;

	/**
	 * Array cu elementele de iterat
	 */
	private $_elements = array();

	/**
	 * Constructor
	 *
	 * @param array $elements Elementele de iterat
	 */
	public function __construct($elements) {
		$this->_elements = $elements;
	}

	/**
	 * Elementul curent
	 *
	 * @return mixed Elementul curent
	 */
	public function current() {
		return $this->_elements[$this->_current];
	}

	/**
	 * Index curent
	 *
	 * @return integer Index curent
	 */
	public function key() {
		return $this->_current;
	}

	/**
	 * Trecerea la index-ul urmator
	 */
	public function next() {
		$this->_current++;
	}

	/**
	 * Resetare index
	 */
	public function rewind() {
		$this->_current = 0;
	}

	/**
	 * Verifica daca elementul curent este setat
	 *
	 * @return boolean Daca elementu curent este setat
	 */
	public function valid() {
          return isset($this->_elements[$this->_current]);
     }
}

// instantiere clasa
$obj = new Iterabil(array(1, 2, 3, 4, 5));

// iterare obiect
foreach ($obj as $value) {
     echo $value.PHP_EOL;
}

// output:
// 1
// 2
// 3
// 4
// 5

Exemplu 2:

Un exemplu putin mai complex, o clasa care permite iterarea prin proprietatile publice ale clasei care o extinde. Se folosesc Iterator si Reflection.

/**
 * Clasa care va genera un obiect cu proprietatile publice iterabile
 */
class Iterabil implements Iterator {

     /**
      * Index pentru elementul iterabil
      */
     private $_current = 0;

     /**
      * Array cu elementele de iterat
      */
     private $_elements = array();

     /**
      * Elementul curent
      *
      * @return mixed Proprietatea curenta in iteratie
      */
     public function current() {
          return $this->_elements[$this->_current]->name;
     }

     /**
      * Index curent
      *
      * @return integer Index curent
      */
     public function key() {
          return $this->_current;
     }

     /**
      * Trecerea la index-ul urmator
      */
     public function next() {
          $this->_current++;
     }

     /**
      * Resetare index si preluare prorietari
      */
     public function rewind() {
          // rewind se apeleaza prima,
          // deci aici ar trebui preluate prorprietatile obiectului
          // se initializeaza obiectul de tip ReflectionClass
          // cu parametru numele clasei curente
          $reflection = new ReflectionClass(get_class($this));

          // se extrag metodele publice
          $this->_elements = $reflection->getProperties(ReflectionMethod::IS_PUBLIC);

          // se seteaza indexul curent
          $this->_current = 0;
     }

     /**
      * Verifica daca elementul curent este setat
      *
      * @return boolean Daca elementu curent este setat
      */
     public function valid() {
         return isset($this->_elements[$this->_current]);
    }
}

/**
 * O noua clasa care va avea proprietati publice
 *
 */
class Testing extends Iterabil {
     public $proprietate1;
     public $proprietate2;
}

// instantiere clasa
$obj = new Testing();

// iterare obiect
foreach ($obj as $value) {
     echo $value.PHP_EOL;
}

// output:
// proprietate1
// proprietate2

Iar daca vrem ca exemplu de mai sus sa fie si accesibil ca un array, nu trebuie decat sa mai implementam din SPL ArrayAccess.

Structura ArrayAccess:

ArrayAccess   {
    /**
     * Verifica daca un offset exista
     */
    abstract public boolean offsetExists ( string $offset );

    /**
     * Intoarce elementul unui offset sau NULL daca nu exista
     */
    abstract public mixed offsetGet ( string $offset );

    /**
     * Seteaza o valoare pentru un offset
     */
    abstract public void offsetSet ( string $offset , string $value );

    /**
     * Dezaloca o valoare pentru un offset
     */
    abstract public void offsetUnset ( string $offset )
}

Exemplu 3:

Un exemplu destul de dificil care are rolul de a evidentia puterea interfetelor din SPL. Obiect iterabil si accesibil ca un array.

Pentru a simplifica logica de acces am folosit chiar functiile pentru parcurgerea unui array (next(), reset()).

/**
 * Clasa care va genera un obiect interabil
 */
class Iterabil implements Iterator, ArrayAccess, Countable {

     /**
      * Array cu elementele de iterat
      */
     private $_elements = array();

     /**
      * Constructor
      *
      * @param array $elements Elementele de iterat
      */
     public function __construct($elements) {
          $this->_elements = $elements;
     }

     /**
      * Elementul curent
      *
      * @return mixed Elementul curent
      */
     public function current() {
          return current($this->_elements);
     }

     /**
      * Index curent
      *
      * @return integer Index curent
      */
     public function key() {
          return key($this->_elements);
     }

     /**
      * Trecerea la index-ul urmator
      */
     public function next() {
          next($this->_elements);
     }

     /**
      * Resetare index
      */
     public function rewind() {
          reset($this->_elements);
     }

     /**
      * Verifica daca elementul curent este setat
      *
      * @return boolean Daca elementu curent este setat
      */
     public function valid() {
          return current($this->_elements)?true:false;
    }
    /**
     * Verifica daca un offset exista
     *
     * @param string $offset Element cautat
     * @return boolean Daca elementul de la offset exista
     */
    public function offsetExists($offset) {
         return isset($this->_elements[$offset]);
    }

    /**
     * Elementul de la un offset
     *
     * @param string $offset Offset array
     * @return mixed
     */
    public function offsetGet($offset) {
         return $this->_elements[$offset];
    }

    /**
     * Setare element in array
     *
     * @param string $offset Offset element
     * @param mixed $value Valoarea pentru elementul din array
     */
    public function offsetSet($offset, $value) {
         $this->_elements[$offset] = $value;
    }

    /**
     * Dezalocare element in array
     *
     * @param string $offset Offset element
     */
    public function offsetUnset($offset) {
         unset($this->_elements[$offset]);
    }

    /**
     * Numarul de elemente din array-ul curent
     *
     * @return integer Nr elemente array
     */
    public function count() {
         return count($this->_elements);
    }
}

// instantiere clasa
$obj = new Iterabil(array(1, 2, 3, 4, 5));

echo 'Iterare folosind "for":'.PHP_EOL;

// iterare ca printr-un array simplu
for($i = 0; $i < count($obj); $i++) {
     echo $obj[$i].PHP_EOL;
}

echo 'Element de sters: '.$obj[1].PHP_EOL;

unset($obj[1]);

echo 'Iterare folosind "foreach":'.PHP_EOL;

// iterare array folosind foreach
foreach ($obj as $element) {
     echo $element.PHP_EOL;
}

// Output:
//Iterare folosind "for":
//1
//2
//3
//4
//5
//Element de sters: 2
//Iterare folosind "foreach":
//1
//3
//4
//5

Written by Claudiu Persoiu

8 October 2009 at 8:38 AM