Генераторы в PHP 5.5

Самым большим нововведением (и очень важным для языка) в PHP 5.5.0 будут генераторы. Я установил себе альфа-версию 5.5.0, хотел посмотреть поближе что да как, генераторы мне нравятся в Пайтоне и я рад, что PHP ими обзаведётся.

Посмотрите на код, который я написал, пока мы ждали всех участников встречи, на которую я был приглашён:

<?
function enumerate($arr)
{
    $i = 0;

    foreach ($arr as $value) {
        yield [$i++, $value];
    }
}

function ifilter(callable $predicate = null, $arr)
{
    if ($predicate === null) {
        $predicate = 'boolval';
    }

    foreach ($arr as $value) {
        if ($predicate($value)) {
            yield $value;
        }
    }
}

function islice($arr, $start, $stop)
{
    if (is_array($arr)) {
        reset($arr);

        for ($i = $start; $i > 0 && each($arr); $i--);

        for ($i = $stop - $start + 1; $i > 0 && list(,$value) = each($arr); $i--) {
            yield $value;
        }
    } else {
        for ($i = $start; $i > 0 && $arr->valid(); $i--) {
            $arr->next();
        }

        for ($i = $stop - $start; $i > 0 && $arr->valid(); $i--) {
            yield $arr->current();
            $arr->next();
        }
    }
}

$array = [10, 20, 30, 33, 40, 50, 60];

$gen = islice(
    enumerate(
        ifilter(
            function($x) { return ($x % 10) === 0; },
            $array
        )),
        1, 4
    );

foreach ($gen as $value) {
    print_r($value);
}

Три генератора, пропускающие через себя данные, обрабатывают массив, всё работает. Названия взяты из Пайтона. Первый нумерует входящий поток данных (возвращает пары номер и значение), второй фильтрует значения через функцию, третий отрезает заданную часть от входных данных.

Концепция генераторов проста, как герань. PHP поддерживает два способа их создания (как и Пайтон), но мне милее оператор yield. Работает он примерно как return, но при повторном входе в функцию её выполнение происходит не сначала, а с того места, откуда управление было возвращено при помощи yield. Можно ещё передавать значения внутрь генератора, но я про это сейчас не буду. Вцелом, получаются настоящие ленивые вычисления — всё это вызывается ровно тогда, когда это реально нужно.

Генераторы, обычно, работают с итерируемыми объектами — возращают их и/или принимают. Итерируемый объект, проще говоря, это то, по чему можно пробежаться циклом. Генераторы, кстати, тоже итерируемые.

И вот тут опять проявляются недостатоки PHP: отсутствие общей логики, непродуманность.

Массив тут до сих пор примитивный тип и он требует иной способ обработки. Посмотрите на генератор islice, тут проблема видна во всей красе. Начинается она уже с хинтинга, я не могу потребовать от $arr быть типа array, потому что на вход может прийти другой генератор, и указать класс генератора тоже не могу — массив ему не принадлежит. Думаю, следует ожидать появления нового типа «iterable», как это уже случилось с callable.

Вторая проблема ещё более серьёзная. У массива нет методов (он же примитивный тип), а генераторы, почему-то, не работают в процедурном стиле. Поэтому мне приходится делить логику на две части: отдельно писать её для массива и для генераторов. Если я задумаю поддержать ещё и строки (в Пайтоне они отлично итерируются), то нужно будет добавить ещё и третью ветку.

Мне кажется, PHP следует развивать класс ArrayObject, который уже присуствует в языке и делать прозрачное преобразование примитивных типов в объекты (как это происходит в JavaScript), это решило бы некоторые проблемы.

6 комментариев

Наведение порядка в PHP (версия 5.5.0)

Нужно ещё похвалить авторов PHP за то, что они действительно стараются навести порядок в языке. С каждой версией он становится немного логичнее.

Например, теперь уже можно сделать вот так:

[
    POW => function($x) { return $x*$x; },
    SIN   => function($x) { return sin($x); },
][$selector]

Правда, всё ещё нельзя вызвать функцию сразу. В 5.5.0 будет работать вот такая вещь:

if (empty(somefunc())) { /* …  */}

Но то же самое нельзя сделать с isset, так как сложно сказать что это будет значить — в языке нет отдельного неопределённого значения переменной, она бывает только недекларированной; функция, конечно, может её вернуть, но на самом деле вернётся NULL, а isset от значения NULL — «ложь».

Появилась возможность использовать list внутри цикла следующим образом:

foreach ($somehash as list($key, $value)) { /* … */ }

И сделано ещё несколько мелочей, но работы впереди ещё много.

8 комментариев