Пишу, по большей части, про историю, свою жизнь и немного про программирование.

Генераторы в 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 комментариев
Karsonito 2012

Спасибо за обзор.

Единственное, что удивило — это нетрадиционность кода в islice:
for ($i = $start; $i > 0 && each($arr); $i-​-​);
for ($i = $stop — $start + 1; $i > 0 && list(,$value) = each($arr); $i-​-​) {
  yield $value;
}
Счетчик уменьшается, номер выбираемого элемента увеличивается. Наверное brainfuck повлиял :-)

Евгений Степанищев (bolknote.ru) 2012

Комментарий для Karsonito:

А в чём тут нетрадиционность и откуда здесь какие-то традиции? Например, во втором цикле, если я сделаю $i < $stop — $start + 1, то значение будет вычисляться на каждой итерации, а мне этого не нужно, зачем мне лишние вычисления? Лишнюю переменную тоже не хочется делать.

А первый цикл сделан единообразно со вторым.

Karsonito 2012

Так вот откуда ноги растут. Действительно имеет смысл.

А традиции просты — отлаживать проще. Во всяком случае мне. Наверное память не тренированная.

dveris-krasnodar.ru/ 2013

интерестно спс

Алексей 2015

День добрый!
А объясните нубу, в чем смысл строчки:
function ifilter(callable $predicate = null, $arr) ?
каким образом может быть не передан первый параметр (callable $predicate) и передан второй?

Евгений Степанищев (bolknote.ru) 2015

Комментарий для Алексей:

Это позволяет захинтовать первый параметр так, чтобы вместо callable можно было передать null.