YaLinqo (LINQ to Objects для PHP) — версия 2.0

в 8:01, , рубрики: functional programming, linq, linq to objects, php, php5, php5.5, похапэ, Программирование, функциональное программирование

YaLinqo (LINQ to Objects для PHP) — версия 2.0Что-что?

LINQ — это штука, которая позволяет писать запросы, чем-то похожие на SQL, прямо в коде. LINQ to Objects, собственно, позволяет писать запросы к объектам, массивам и всему тому, чем вы оперируете в коде.

Это ещё зачем?

Если у вас есть база, то у вас есть любимый ORM (или любимый голый SQL — кому как по вкусу). Но иногда объекты приходят из веб-сервисов, из файлов, да и вообще тьма тьмущая объектов может требовать нетривиальной обработки: преобразование, фильтрация, сортировка, группировка, агрегация… Применить бы привычный ORM или SQL — но базы-то нет. Тут на помощь приходит LINQ to Objects, в данном случае YaLinqo.

Что умеет?

  • Самый полный порт .NET LINQ на PHP, со многими дополнительными методами. Всего реализовано более 70 методов.
  • Ленивые вычисления, текст исключений и многое другое, как в оригинальном LINQ.
  • Детальная документация PHPDoc к каждому методу. Текст статей адаптирован из MSDN.
  • 100% покрытие юнит-тестами.
  • Коллбэки можно задавать замыканиями, «указателями на функцию» в виде строк и массивов, строковыми «лямбдами» с поддержкой нескольких синтаксисов.
  • Ключам уделяется столько же внимания, сколько значениям: преобразования можно применять и к тем, и к другим; большинство коллбэков принимает на вход и то, и другое; ключи по возможности не теряются при преобразованиях.
  • Минимальное изобретение велосипедов: для итерации используются Iterator, IteratorAggregate и др. (и их можно использовать наравне с Enumerable); исключения по возможности используются родные похапэшные и т.п.
  • Поддерживается Composer, есть пакет на Packagist.
  • Никаких внешних зависимостей.

Что случилось?

Прошёл год, как вышел PHP 5.5 со всякими вкусностями типа генераторов и исправленных итераторов. Так как на моей совести самый полноценный порт LINQ на PHP, то я решил, что настало время его обновить и воспользоваться новыми фичами языка.

Что нового?

Скорость новая. Выкинуты тонны кода (порядка 800 строк под подсчётам Git): не стало костылей вроде Enumerator для генерации Iterator'ов; не стало бесполезных коллекций, по сути единственным преимуществом которых было хранение объектов в ключах; не стало call_user_func… И самое главное — не стало кучи маловменяемого кода для генерации итераторов, который невозможно понять. Остался foreach и yield. Всё это в сумме дало нехилый прирост в скорости.

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

return new Enumerable(function () use ($self, $inner, $outerKeySelector, $innerKeySelector, $resultSelectorValue, $resultSelectorKey)
{
    /** @var $self Enumerable */
    /** @var $inner Enumerable */
    /** @var $arrIn array */
    $itOut = $self->getIterator();
    $itOut->rewind();
    $lookup = $inner->toLookup($innerKeySelector);
    $arrIn = null;
    $posIn = 0;
    $key = null;

    return new Enumerator(function ($yield) use ($itOut, $lookup, &$arrIn, &$posIn, &$key, $outerKeySelector, $resultSelectorValue, $resultSelectorKey)
    {
        /** @var $itOut Iterator */
        /** @var $lookup YaLinqocollectionsLookup */
        while ($arrIn === null || $posIn >= count($arrIn)) {
            if ($arrIn !== null)
                $itOut->next();
            if (!$itOut->valid())
                return false;
            $key = call_user_func($outerKeySelector, $itOut->current(), $itOut->key());
            $arrIn = $lookup[$key];
            $posIn = 0;
        }
        $args = array($itOut->current(), $arrIn[$posIn], $key);
        $yield(call_user_func_array($resultSelectorValue, $args), call_user_func_array($resultSelectorKey, $args));
        $posIn++;
        return true;
    });
});

на лаконичное:

return new Enumerable(function () use ($inner, $outerKeySelector, $innerKeySelector, $resultSelectorValue, $resultSelectorKey) {
    $lookup = $inner->toLookup($innerKeySelector);
    foreach ($this as $ok => $ov) {
        $key = $outerKeySelector($ov, $ok);
        if (isset($lookup[$key]))
            foreach ($lookup[$key] as $iv)
                yield $resultSelectorKey($ov, $iv, $key) => $resultSelectorValue($ov, $iv, $key);
    }
});

Кроме этого, я наконец-то добавил человеческие теги с версиями в репозиторий и описал алиасы веток в composer.json, поэтому работа с Composer теперь должна вызывать меньше боли.

Так что это такое, в конец-то концов?

Допустим, у вас есть массивы:

$products = array(
    array('name' => 'Keyboard',    'catId' => 'hw', 'quantity' =>  10, 'id' => 1),
    array('name' => 'Mouse',       'catId' => 'hw', 'quantity' =>  20, 'id' => 2),
    array('name' => 'Monitor',     'catId' => 'hw', 'quantity' =>   0, 'id' => 3),
    array('name' => 'Joystick',    'catId' => 'hw', 'quantity' =>  15, 'id' => 4),
    array('name' => 'CPU',         'catId' => 'hw', 'quantity' =>  15, 'id' => 5),
    array('name' => 'Motherboard', 'catId' => 'hw', 'quantity' =>  11, 'id' => 6),
    array('name' => 'Windows',     'catId' => 'os', 'quantity' => 666, 'id' => 7),
    array('name' => 'Linux',       'catId' => 'os', 'quantity' => 666, 'id' => 8),
    array('name' => 'Mac',         'catId' => 'os', 'quantity' => 666, 'id' => 9),
);
$categories = array(
    array('name' => 'Hardware',          'id' => 'hw'),
    array('name' => 'Operating systems', 'id' => 'os'),
);

Допустим, вам нужно разместить продукты с ненулевым количеством в соответствующие отсортированные по имени категории, и внутри категорий отсортировать продукты сначала по убыванию количества, потом по имени. Сейчас вы начинаете строить в уме трижды вложенные циклы, вызовы функций для массивов, пытаетесь вспомнить, какой префикс у подходящей функции сортировки… Вместо всего этого можно написать:

$result = from($categories)
    ->orderBy('$cat ==> $cat["name"]')
    ->groupJoin(
        from($products)
            ->where('$prod ==> $prod["quantity"] > 0')
            ->orderByDescending('$prod ==> $prod["quantity"]')
            ->thenBy('$prod ==> $prod["name"]'),
        '$cat ==> $cat["id"]', '$prod ==> $prod["catId"]',
        '($cat, $prods) ==> [
            "name" => $cat["name"],
            "products" => $prods
        ]'
    );

Если бы создатели PHP не были упёртыми ослами и не отказались от пулл-реквеста с лямбдами, можно было бы написать даже так:

$result = from($categories)
    ->orderBy($cat ==> $cat['name'])
    ->groupJoin(
        from($products)
            ->where($prod ==> $prod['quantity'] > 0)
            ->orderByDescending($prod ==> $prod['quantity'])
            ->thenBy($prod ==> $prod['name']),
        $cat ==> $cat['id'], $prod ==> $prod['catId'],
        ($cat, $prods) ==> [
            'name' => $cat['name'],
            'products' => $prods,
        ]
    );

Так или иначе, на выходе мы получим:

Array (
    [hw] => Array (
        [name] => Hardware
        [products] => Array (
            [0] => Array ( [name] => Mouse       [catId] => hw [quantity] =>  20 [id] => 2 )
            [1] => Array ( [name] => CPU         [catId] => hw [quantity] =>  15 [id] => 5 )
            [2] => Array ( [name] => Joystick    [catId] => hw [quantity] =>  15 [id] => 4 )
            [3] => Array ( [name] => Motherboard [catId] => hw [quantity] =>  11 [id] => 6 )
            [4] => Array ( [name] => Keyboard    [catId] => hw [quantity] =>  10 [id] => 1 )
        )
    )
    [os] => Array (
        [name] => Operating systems
        [products] => Array (
            [0] => Array ( [name] => Linux       [catId] => os [quantity] => 666 [id] => 8 )
            [1] => Array ( [name] => Mac         [catId] => os [quantity] => 666 [id] => 9 )
            [2] => Array ( [name] => Windows     [catId] => os [quantity] => 666 [id] => 7 )
        )
    )
)

Для эстетов и оптимизаторов есть возможность использовать анонимные функции, а не «строковые лямбды». Вместо '$prod ==> $prod["quantity"] > 0' можете писать function ($prod) { return $prod['quantity'] > 0; }. Для диких обфускаторов есть возможность использовать имена аргументов по умолчанию (v — значение, k — ключ и т.п.), то есть можно писать просто '$v["quantity"] > 0' (для сложных вложеных запросов не рекомендуется).

А где LINQ to Database?

Да, вообще-то в мире .NET запросы LINQ используются и в ORM (кто-то скажет, что это вообще основное назначение), но конкретно эта фича в библиотеке отсутствует, потому что любая попытка её реализовать выльется в тонну костылей, тормозов и прочих неприглядных вещей из-за отсутствия поддержки на уровне языка (разбора выражений, в частности). LINQ to Objects-то не обошёлся без костылей в виде «строковых лямбд», а тут полноценный транслятор из PHP в SQL с полным разбором и тоннами оптимизаций понадобится — закат солнца вручную.

Давай!

P.S. Старая версия поддерживает PHP 5.3. Функционально не уступает, но несколько тормознее (итераторы-с).

P.P.S. Наконец-то появился вменяемый конкурент (Ginq). В нём тонны SPL, Symfony и прочей арихитектуры, ноль комментариев, много тормозов (оверхед от x2 до x50 относительно моей версии). Бенчмарки в процессе, напишу в следующий раз.

Автор: Athari

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js