Generator API

Issue #27 resolved
Former user created an issue

Здравствуйте.

Сейчас интерфейс ev построен на колбеках, это не очень удобно, когда работаешь с генераторами, нельзя передать как колбек [$generator, 'send'] потому что метод send может принимать только один аргумент.

Я предлагаю, разрешить использовать в качестве колбека объект Generator и вызывать его метод send($data) это будет очень удобно, это возможно сделать?

Спасибо.

Comments (12)

  1. Ruslan Osmanov repo owner

    Здравствуйте.

    А в каких случаях может быть необходимо передавать Generator::send в качестве колбека? Например, в следующем скрипте мы без проблем пользуемся API генераторов:

    <?php
    $gen = (function () {
        for ($i = 0; (yield $i++) < 10;);
    })();
    
    $timer = new EvTimer(3, 0, function () use ($gen) {
        $gen->send(20);
    });
    
    $idle = new EvIdle(function ($w) use ($gen) {
        if (!$gen->valid()) {
            $w->stop();
            return;
        }
    
        echo $gen->current(), PHP_EOL;
        sleep(1);
        $gen->next();
    });
    
    Ev::run();
    
    0
    1
    2
    

    Кроме того, в своём колбеке мы имеем возможность управлять объектом EvWatcher. В случае же метода Generator::send мы бы лишь слепо отправляли в генератор... данные, прикрепленные к объекту EvWatcher?

  2. Alex Sky

    У вас отличный пример который показывает сколько лишнего кода нужно писать чтобы использовать генераторы.

    В примере создаются два объекта Closure (замыкание) которые здесь нужны только как обвертка для работы с объектами Generator.

    Замыкание, здесь не нужно, это лишний ненужный код, который кстати добавляет ещё одно звено в стек вызовов, потребляют память.

    Вот код, без замыканий и со всеми возможностями по управлению объектами EvWatcher

    <?php
    
    class Test
    {
        public $idle;
        public $timer;
    
        public function gen()
        {
            for ($i = 0; (yield $i++) < 10;);
        }
    
        public function idle($gen)
        {
            $data = yield;
    
            if (!$gen->valid()) {
                $this->idle->stop();
                return;
            }
    
            echo $gen->current(), PHP_EOL;
            sleep(1);
            $gen->next();
        }
    }
    
    $obj = new Test;
    
    $gen = $obj->gen();
    $obj->timer = new EvTimer(3, 0, $gen);
    $obj->idle  = new EvIdle($obj->idle($gen));
    
    Ev::run();
    

    Проблема не только в том что замыкания лишние звено, мы сейчас не можем в callable хендлерах использовать оператор yield потому что это уже будет не замыкания а генератор, с которым напрямую работать Ev пока ещё не умеет, но надеюсь это можно сделать.

  3. Ruslan Osmanov repo owner

    Я всё равно не понимаю идеи.

    Колбеки по определению являются обработчиками событий. Генераторы по определению являются источниками данных. Генераторы не являются представителями типа callable.

    Я не вижу проблемы в том, что при необходимости получить очередную порцию данных в ответ на событие, мы делаем это в колбеке, дёргая генератор. Лишь в одном редком случае мы можем попытаться увидеть в этой схеме проблему - когда единственной функцией колбека является получение очередной порции данных из генератора, причём без сохранения этих данных где-либо, кроме как в самом генераторе, - это разве что оверхед вызова функции. Я не измерял, но сомневаюсь, что оверхед вызова функции больше оверхеда вызова Generator::next().

    Кроме того, в случае $obj->timer = new EvTimer(3, 0, $gen); мы таки не имеем возможности управлять объектом таймера в момент обработки события "прошло 3 секунды". Конечно, мы могли бы управлять таймером откуда угодно, воспользовавшись областью видимости класса или use. Но момент, ближайший к возникновению события, заблокирован ограниченным характером генератора.

    Таким образом, я пока не вижу необходимости в поддержке такой функции, извините.

  4. Alex Sky

    Я всё равно не понимаю идеи.

    Идея простая, мы используем генераторы для реализации async/await API.

    Колбеки по определению являются обработчиками событий

    Да, в таких языках как Python, Node.js, обработчиками событий могут быть async function которые по сути являются генераторами (корутинами). В РНР пока что для async function приходится использовать генераторы, ещё нет синтекс сахара async/await.

    Кроме того, в случае $obj->timer = new EvTimer(3, 0, $gen); мы таки не имеем возможности управлять объектом таймера в момент обработки события

    Возможность эта есть.

    <?php
    
    class Test
    {
        public $idle;
        public $timer;
    
        public function gen()
        {
            for ($i = 0; (yield $i++) < 10;);
    
            $this->timer->stop();
        }
    }
    

    Генераторы даже более удобны для замыкания внешних переменных, вот простой пример:

    <?php
    
    $a = 'a';
    $b = 'b';
    $c = 'c';
    
    $callable = function() use($a, $b, $c) { ... };
    $gen = (function($a, $b, $c) { yield })($a, $b, $c);
    

    В конструктор генератора отправляются ($a, $b, $c), с замыканием нужно использовать use($a, $b, $c)

    Таким образом, я пока не вижу необходимости в поддержке такой функции, извините.

    Очень жаль, это означает что мы по прежнему должны оборачивать генераторы в замыкания, чтобы использовать генераторы для async/await функций.

  5. Alex Sky

    Ок, попробую, если я правильно понял, в этой ветке можно будет использовать [$gen, 'send']?

  6. Ruslan Osmanov repo owner

    Ок, попробую, если я правильно понял, в этой ветке можно будет использовать [$gen, 'send']?

    Да:

    <?php
    $gen = (function () {
        for (;;)
            var_dump((yield));
    })();
    
    $timer = new EvTimer(0, 1, [$gen, 'send'], 'user data');
    Ev::run();
    
    object(EvTimer)#3 (6) {
      ["repeat"]=>
      float(1)
      ["remaining"]=>
      float(0.99890625599801)
      ["is_active"]=>
      bool(true)
      ["data"]=>
      string(9) "user data"
      ["is_pending"]=>
      bool(false)
      ["priority"]=>
      int(0)
    }
    ...
    
  7. Alex Sky

    Пытаюсь проверить новую версию из ветки issue27 Но все равно выдает ошибку PHP Warning: Generator::send() expects exactly 1 parameter, 2 given in ...

    Возможно это у меня кривые руки, собираю модуль так:

    cd /opt
    git clone https://ua-san@bitbucket.org/osmanov/pecl-ev/branch/issue27.git
    cd issue27
    phpize
    ./configure
    make && make install
    cp /opt/issue27/modules/ev.so /usr/lib64/php/modules/ev.so
    

    Я правильно все делаю? :)

  8. Ruslan Osmanov repo owner

    Наверно это Mac, а на Mac может быть всё. Поэтому вот точные команды, которые в Bash должны работать.

    git clone https://bitbucket.org/osmanov/pecl-ev.git
    cd pecl-ev
    git checkout issue27
    phpize
    ./configure
    make
    (
    cat <<'EOS'
    <?php
    $gen = (function () { for (;;) var_dump((yield)); })();
    $timer = new EvTimer(0, 1, [$gen, 'send'], 'user data');
    Ev::run();
    EOS
    ) >gen.php
    php -n -d extension=ev.so  -dextension_dir=./.libs gen.php
    
  9. Alex Sky

    Отлично, работает!

    Прогнал в бенчмарке, вызов метода как callable [$gen, 'send'] в 2 раза медленней, относительно прямого вызова $gen->send() это не проблема Ev это в ядре РНР.

    В любом случаи спасибо. Надеюсь в будущем в РНР появятся нативные async функции которые можно использовать как обработчики событий.

  10. Alex Sky

    В РНР 7.1 есть возможность ускорить вызов Closure::fromCallable([$gen, 'send'])

  11. Log in to comment