Colada — удобная работа с коллекциями

в 13:05, , рубрики: collections, php, метки: ,

Colada — библиотека для удобной и безопасной работы с коллекциями в PHP.

Это, прежде всего, работа в объектно-ориентированном стиле (что из коробки в PHP довольно неудобно). Немодифицируемые коллекции, защита от NPE (Null Pointer Exception) при помощи опциональных значений (optional values), любые значения (а не только скаляры) для ключений в map'ах — это всё об этом.

Установка

Установить библиотеку через Composer так же просто, как… Как и подключить её в виде git submodule :)

{
    "require": {
        "alexeyshockov/colada": "dev-master"
    }
}

$ git submodule add git://github.com/alexeyshockov/colada.git vendor/colada

Коллекции

Как же воспользоваться Colada после установки? Есть несколько вариантов, самый простой из которых: сделать коллекцию из обычного массива, который в большинстве случаев уже имеется в налии, и с которым нужно произвести какие-то модификации:

$counts = to_collection(array(2, 3, 50, 36));
$users  = to_set(array('label1', 'label2', 'label3'));

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

$counts = collection(2, 3, 50, 36);
$users  = set('label1', 'label2', 'label3');

Почти как стандартный array()!

mapBy(), acceptBy()/rejectBy(), foldBy()

Хорошее описание функционального стиля работы с коллекциями уже довольно неплохо дано в статье «Что не так с циклами for?». В Colada map, accept/reject и fold имеют точно такой же смысл, как и во всех остальных библиотеках для работы с коллекциями (Underscore.js, Scala Collections и т.д.).

Рассмотрим пример:

$saleCount = $users
    ->acceptBy(x()->isActive())
    ->mapBy(x()->getSales())
    ->foldBy(function($count, $posts) {
        return $count + count($posts);
    , 0);

Надеюсь, код выше говорит сам за себя, и его смысл понятен по ходу чтения, за исключением… Что за x()?

x()

Тем, у кого есть опыт программирования на Scala или подобном языке, в котором работа с замыканиями изначально делалась максимально удобной, это может показаться знакомым.

Scala:

val emails = users.map(x.getEmail());

PHP (Colada):

$emails = $users->mapBy(x()->getEmail());

В PHP работа с замыканиями несколько более многословна, чем в том же Scala. Но для большинства случаев, когда нужно просто вызвать getter у объекта в коллекции, всё можно упростить, как и показано выше.

x() — это будущий элемент коллекции. Использовние его эквивалентно простому замыканию:

$emails = $users->mapBy(function($user) {
    return $user->getEmail()
});

Вы всё ещё пишите замыкание для каждой мелочи? Тогда мы идём к вам! ;)

Хеши (a.k.a. maps)

Если коллекции — это просто наборы элементов, то хеши представляют из себя пары ключ/значение, с которыми мы так же сталкиваемся каждый день. Как и для обычных коллекций, мы можем создать хеш

$users = to_map(array('managers' => array(1, 3), 'users' => array(2, 3, 4)));

и работать с ним в объектно-ориентированном стиле, используя знакомые mapBy() и acceptBy()/rejectBy(), а так же другие полезные и удобные методы: mapElementsBy(), flip(), pick().

Кто-то сказал, что один листинг кода иногда бывает лучше тысячи слов:

$usersByGroups = $users
    ->mapBy(x()->isActive())
    ->groupBy(x()->getGroup())
    ->mapElementsBy(x()->count());

foreach ($groups as $group) {
    echo 'Group "'$group->getName().'" contains '.$usersByGroups->get($group)->orElse(0).' users.';
    echo "n";
}

В коде выше можно заметить одну особенность: не считая «сахарных» методов, которые упрощают работу с коллекциями и хешами, Map::get() возвращает не само значение по ключу, а объект-обёртку некоего класса Option, о котором я не упоминал до текущего момента.

Optional Values

Сэр Энтони Хоар, которому приписывают введение NULL-значений, как-то сказал:

I call it my billion-dollar mistake.

На тему защиты от NPE (Null Pointer Exception) было сказано много слов и придумано несколько решений. Одним из них является введение так называемых опциональных значений. В некоторых языках они есть изначально: в Haskell это Maybe, в Scala — Option. Для остальных языков существуют библиотеки, как, например, Optional из Google Guava для Java.

Опциональные значения в Colada очень похожи на свой аналог из Scala. Some означает наличие значения, None — его отсутствие. В использовании опциональные значения очень походи на коллекции (упрощая, можно думать, что возвращается коллекция из одного или ноля элементов):

echo "Город: ";
echo $user->getCity()->mapBy(x()->getName())->orElse('-');

На примере выше показано, как может применяться опциональное значение в классах модели предметной области. В данном случае, при работе с городом пользователя, мы никогда не забудем, что он может быть и не заполнен. Точно так же мы никогда не забудем, что запрашиваемый ключ может вообще не существовать в данном хеше.

Make me unsee it

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

foreach ($groups as $group) {
    echo 'Group "'$group->getName().'" contains '.$usersByGroups->apply($group).' users.';
    echo "n";
}

И словить исключение, если элемента таки не оказалось.

Использование опциональных значений — довольно обширная тема, детальное описание конторой дело отдельной статьи.

Производительность и использование памяти

Высокая производительность и оптимальное использование памяти изначально не были главными целями — во главу угла было поставлено удобство использования. В подавляющем большинстве случаев работа происходит с небольшими коллекциями, поэтому разница в производительности не будет заметной.

Тем не менее, даже при таком раскладе результат получился вполне себе, на мой взгляд. Благодаря использованию SplFixedArray для реализации коллекций, использование памяти даже меньше по сравнению с обычными массивами:

Использование памяти на примере коллекции из 100 000 элементов

$ ./shell.sh 
// Call use_colada() function to benefit from all shortcuts ;)
Interactive shell

php > use_colada();
php > vd(memory_get_usage(), memory_get_peak_usage());
int(374736)
int(383040)

php > $nums = Collections::range(1, 100000);
php > vd(memory_get_usage(), memory_get_peak_usage());
int(3575236)
int(3581604)

php > $nums = range(1, 100000);
php > vd(memory_get_usage(), memory_get_peak_usage());
int(8099280)
int(8105996)

В части скорости работы никаких чудес — за удобство использования приходится платить… Расширенное сравнение элементов при поиске (Collection::contains(), Map::get()...), а так же остальные плюшки пока реализованы в лоб и, естественно, работают медленнее встроенных аналогов. Но всё может измениться к лучшему, в том числе и с вашей помощью ;)

Но и здесь есть то, что уже оптимизировано, и о чём нужно помнить и не бояться использованить — цепочки преобразований из mapBy() и acceptBy()/rejectBy() ленивы по умолчанию! Это очень знакомо людям, которые имели опыт программирования на функциональных языках, и наглядно видно на примере:

$emails = $users
    ->acceptBy(x()->isActive())
    ->mapBy(x()->getEmail());

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

$emails = $users
    ->acceptBy(x()->isActive())
    ->mapBy(x()->getEmail());

foreach ($emails as $email) {
    echo $email;
}

«Конкуренты»

Вдохновившись языком Scala (Scala Collections) и библиотекой Underscore.js, я с самого начала не мог представить, что чего-то подобного нет для PHP. Но либо я плохо искал, либо действительно на момент разработки не было решений, предоставляющих такое же удобство в PHP.

Тем не менее, некоторые наработки есть и их я хотел бы привести здесь, чтобы каждый мог сделать сравнение и выбрать подходящий инструмент.

Итак, что же уже есть на просторах Интернета:

  • Collection в Doctrine Common — вспомогательная обёртка для работы с коллекциями, разработанная для внутреннего использования в проектах Doctrine и не особо известная за их пределами. Предоставляет, по сути, простую объектно-ориентированную обёртку на старндартным массивом, не обладая какими-либо дополнительными свойствами (immutable, нескалярные ключи и т.д.).
  • Underscore.php — прямой аналог Underscore.js для PHP, сделанный, на мой взгляд, практически без учёта специфики языка.
  • Rmk-Framewrok (с описанием на Хабре) — примечательная разработка от нашего соотечественника, ориентированная прежде всего на предоставление различных структур данных, не реализованных в PHP по умолчанию. Библиотека приследуюет изначально другую цель, нежели Colada, поэтому проигрывает при субъективном сравнение удобства использования.

В заключении

Полезные (возможно) сылки:

P.S.

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

Автор: alexeyshockov

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


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