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('-');
На примере выше показано, как может применяться опциональное значение в классах модели предметной области. В данном случае, при работе с городом пользователя, мы никогда не забудем, что он может быть и не заполнен. Точно так же мы никогда не забудем, что запрашиваемый ключ может вообще не существовать в данном хеше.
foreach ($groups as $group) {
echo 'Group "'$group->getName().'" contains '.$usersByGroups->apply($group).' users.';
echo "n";
}
И словить исключение, если элемента таки не оказалось.
Использование опциональных значений — довольно обширная тема, детальное описание конторой дело отдельной статьи.
Производительность и использование памяти
Высокая производительность и оптимальное использование памяти изначально не были главными целями — во главу угла было поставлено удобство использования. В подавляющем большинстве случаев работа происходит с небольшими коллекциями, поэтому разница в производительности не будет заметной.
Тем не менее, даже при таком раскладе результат получился вполне себе, на мой взгляд. Благодаря использованию SplFixedArray для реализации коллекций, использование памяти даже меньше по сравнению с обычными массивами:
$ ./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, поэтому проигрывает при субъективном сравнение удобства использования.
В заключении
Полезные (возможно) сылки:
- github.com/alexeyshockov/colada — исходный код (Pull Request'ы приветствуются)
- alexeyshockov.github.com/colada — документация по API
P.S.
С первого взгляда это всё может показаться бредом, а код — написанным уж точно не на PHP. Если кого-то это всё таки заинтересует, я рассчитываю на конструктивные комментарии, с помощью которых я смогу раскрыть те вещи, которые описал недостаточно полно.
Автор: alexeyshockov