Seit PHP 5.5 gibt es eine Reihe großartiger neuer Features. In diesem Artikel werden wir hauptsächlich auf Generatoren eingehen. Weitere Features werden vereinzelt im Laufe des Artikels an Beispielen aufgezeigt.
Wer die Generatorsyntax nicht schon aus anderen Sprachen, wie zum Beispiel Python, kennt, wird hier einen kleinen Einblick in die Funktionsweise und den Nutzen erhalten.
Grundsätzlich gilt, dass Generatoren in erster Linie dem vereinfachten Formulieren von Iteratoren dienen und deswegen auch das Iterator Interface implementieren.
Genau wie Iteratoren werden Generatoren üblicherweise in einem „foreach
“ Konstrukt genutzt. Die Generatorsyntax besteht dabei hauptsächlich aus dem neuen Keyword „yield
“, welches innerhalb von Funktionen ganz ähnlich dem „return
“ Keyword genutzt werden kann. Im Gegensatz zum return
, beendet yield
die Funktion allerdings nicht, sondern pausiert die Funktion und gibt die Kontrolle zurück an die aufrufende foreach
Schleife. Bei jedem Schleifendurchlauf wird die Generatorfunktion wieder aufgenommen und fortgesetzt bis das nächste yield
erreicht wurde, die Funktion am Ende angekommen ist oder durch ein return
vorzeitig beendet wurde.
Ein simples Beispiel einer Funktion, die einen Dateinamen nimmt und jede Zeile der Datei mit yield
zurückgibt, ist folgendes:
function ReadLines($filename) { $fh = fopen($filename, 'r'); if(!$fh) { return; } while(!feof($fh)) { yield fgets($fh); } fclose($fh); } foreach(ReadLines('somefilename.txt') as $line) { echo $line; }
Hier öffnet readLines die Datei, liest mit fgets()
eine Zeile aus und gibt sie mit yield
an die aufrufende foreach
Schleife weiter. Die Funktion wird pausiert und echo $line
wird aus dem Schleifenrumpf ausgeführt. Anschließend wird zum Schleifenkopf zurückgekehrt und die Funktion bis zum nächsten yield
fortgesetzt.
Die Alternativen wären zum einen das Sammeln sämtlicher Zeilen in einem Array, was bei großen Dateien allerdings problematisch wäre, und zum anderen das Implementieren eines Iterators. Einen Vergleich zu Iteratoren mit einem ähnlichen Beispiel gibt es in den PHP Docs.
Der wesentliche Vorteil von Generatoren ist ihre Einfachheit. Einige Features, die von Iteratoren implementiert werden können, gibt es in Generatoren beispielsweise nicht (z.B. kann man die rewind()
Methode nicht auf einen bereits gestarteten Generator anwenden).
In einem etwas realistischeren Beispiel wollen wir CSV-Daten aus einer Datei auslesen:
function CsvReader($fh, $delimiter=',') { while(!feof($fh)) { yield fgetcsv($fh, 0, $delimiter); } }
Da das ganze etwas langweilig wirkt, erweitern wir die Funktion um ein Feature: Wir erwarten, dass die erste Zeile in der CSV-Datei die Feldnamen enthält und geben mit jeder weiteren Zeile ein assoziatives Array zurück, welches die Feldnamen als Key nutzt.
function CsvReader($fh, $delimiter=',') { $fields = fgetcsv($fh, 0, $delimiter); while(!feof($fh)) { $csvrow = fgetcsv($fh, 0, $delimiter); $row = []; for($i=0; $i<count($csvrow); $i++) { $row[$fields[$i]] = $csvrow[$i]; } yield $row; } }
Für demonstrative Zwecke werden wir auch die for-Schleife in der Funktion durch einen neuen Generator ersetzen. Dazu implementieren wir die enumerate() Funktion, die bereits aus Python bekannt sind:
function Enumerate($iterable, $start=0) { foreach($iterable as $key => $value) { yield $key => [$start++, $value]; } }
Dabei iterieren wir über jeden Wert eines bestehenden Arrays/Iterators und geben mit jedem Durchlauf den originalen Wert mit einer fortlaufenden Nummer zurück. Eingesetzt sieht das dann folgendermaßen aus:
function CsvReader($fh, $delimiter=',') { $fields = fgetcsv($fh, 0, $delimiter); while(!feof($fh)) { $csvrow = fgetcsv($fh, 0, $delimiter); $row = []; foreach(Enumerate($csvrow) as list($i, $column)) { $row[$fields[$i]] = $column; } yield $row; } }
Bei genauerem Hinschauen erkennt man, dass ein weiteres Feature aus PHP 5.5 genutzt wurde: Das Verwenden von list()
in foreach-Schleifen erlaubt das direkte „Entpacken“ von Arrays im Schleifenkopf.
Eine weitere nützliche Funktion, die aus Python bekannt ist und welche wir als Generator in PHP implementieren möchten, ist zip()
.
Zip
verhält sich ganz ähnlich der array_map()
Funktion, wenn man dem callable Argument null
übergibt und mehr als ein Array angibt.
Wesentliche Unterschiede ergeben sich aus den folgenden Gründen: array_map
gibt ein Array zurück und akzeptiert nur Arrays als Argumente, während zip
alles „iterierbare“ als Argument annimmt. Das macht array_map
ungeeignet für „stream“-ähnliche Konstrukte. Außerdem iteriert array_map
solange, bis das längste Array erschöpft ist, wohingegen zip
bis zum Erschöpfen des kürzesten Arrays iteriert.
Zusätzlich möchten wir noch herausfinden, wie man über Strings iteriert. Dafür können wir ebenfalls einen einfachen Generator einsetzen, natürlich mit optionaler Angabe einer Kodierung:
function IterStr($str, $encoding=null) { if(is_null($encoding)) { $encoding = mb_internal_encoding(); } for($i=0; $i<mb_strlen($str, $encoding); $i++) { $char = mb_substr($str, $i, 1, $encoding); yield $char; } }
Ein weiteres Feature, das es seit PHP 5.6 gibt, sind sogenannte „variadische Funktionen“. Diese können eingesetzt werden, um eine beliebige Anzahl von Argumenten in einer Funktion zu akzeptieren, statt auf „func_get_args()“ zurückgreifen zu müssen. Unsere zip
Funktion wird beliebig viele Argumente akzeptieren, deswegen werden wir das neue Feature als eine variadische Funktion definieren.
function Zip(...$arrays) { /** @var \Iterator[] $arrIter */ $arrIter = []; // Convert all non-iterators to iterator, to get a unified interface. foreach(Enumerate($arrays, 1) as list($argIndex, $array)) { if ($array instanceof \Iterator) { $arrIter[] = $array; } else if(is_array($array)) { $arrIter[] = new \ArrayIterator($array); } else if(is_string($array)) { $arrIter[] = IterStr($array); } else { throw new \InvalidArgumentException( "Argument #".$argIndex." is of type '".gettype($array)."'."); } } while(true) { $tpl = []; foreach($arrIter as $iter) { if(!$iter->valid()) { return; // end of iterator. No more yields. } $tpl[] = $iter->current(); $iter->next(); } yield $tpl; } }
Zum Ablauf: Als Erstes nehmen wir alle übergebenen Argumente und erzeugen ein Array von Iteratoren, die alle das Iterator Interface implementieren. Dafür nutzen wir ArrayIterator für Arrays und unseren neuen Generator IterStr für Strings. Generatoren implementieren das Iterator Interface bereits.
In der Endlosschleife holen wir aus jedem Iterator das nächste Element, bis einer der Iteratoren erschöpft ist. Es ist durchaus möglich, dass die Zip
Funktion zu einer Endlosschleife wird, wenn alle Iteratoren endlos laufen, z.B. wenn ein Generator lediglich „while(true) yield;
“ oder ähnliches enthält. Das ist aber beabsichtigt und kann in einigen Szenarien durchaus sinnvoll sein.
Jetzt, wo wir die Zip
Funktion haben, können wir in unseren kleinen CsvReader
die Enumerate
Funktion ersetzen:
function CsvReader($fh, $delimiter=',') { $fields = fgetcsv($fh, 0, $delimiter); while(!feof($fh)) { $csvrow = fgetcsv($fh, 0, $delimiter); $row = []; foreach(Zip($fields, $csvrow) as list($field, $column)) { $row[$field] = $column; } yield $row; } }
Außerdem können wir jetzt auch array_map
als Generator definieren. Wir nutzen jedoch Zip
und können Map somit nur mit der Limitierung implementieren, dass nur bis zum Ende des kürzesten Argumentes iteriert wird:
function Map($callback, ...$iterables) { foreach(Zip(...$iterables) as $args) { yield $callback(...$args); } }
Hier sieht man auch schön, dass man die „...
“ Notation auch beim Aufruf von Funktionen nutzen kann, um Arrays oder Iteratoren auf Funktionsargumente zu „entpacken“, statt „call_user_func_array
“ zu nutzen.
Einen themenspezifischen Beitrag, auf den wir gerne aufmerksam machen möchten, handelt von „Cooperative multitasking“ (englisch) und ist im Dezember 2012 erschienen. Nikita Popov zeigt darin auf, wie man Generatoren als Coroutinen nutzen kann, um kooperatives (non-preemptives) Multitasking zu implementieren. Die gleiche Technik wird in der Python library asyncio genutzt, um das Programmieren von „Callbackhell-freien“ Asynchronen Netzwerkanwendungen zu ermöglichen.
Wenn ihr Fragen zum Thema habt, könnt ihr uns gerne kontaktieren.
Euer Team von Lichtflut.Medien
www.lichtflut-medien.de