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

    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. Если кого-то это всё таки заинтересует, я рассчитываю на конструктивные комментарии, с помощью которых я смогу раскрыть те вещи, которые описал недостаточно полно.
    Поделиться публикацией

    Комментарии 29

      0
      Вау! Это замечательно. Функциональное программирование на PHP это круто ) А ещё круто, что ваше решение очень компактно и синтаксически вкусно. Есть конечно, небольшая проблема с тем, что юзаются функции, но думаю в 90% это не вызовет проблем.
        +2
        Функций там всего несколько, и они служат обёрткой на Builder'ами:

        function to_collection($data)
        {
            $builder = new CollectionBuilder();
        
            return $builder->addAll($data)->build();
        }
        

        Т.е. можно использовать Builder'ы сами по себе, не касаясь функций.

        Эту часть в статье я как раз не раскрыл, т.к. подумал, что будет слишком для одного раза :)
          +3
          Если кого то беспокоят функции, то их можно вынести в фабричные методы, например. По логике как раз подходит, раз уж все равно в них только создание объекта. Но с функциями запись короче, я бы сделал и тот и тот вариант, на любителя :)
        +10
        Вот честно… Вроде как смысл таких библиотек — упрощение синтаксиса работы с коллекциями.
        Но, ИМХО, обычный for/foreach гораздо читабельнее, чем приведенные выше конструкции.
        Возможно это дело привычки, но прочитанный код мне кажется совсем неочевидным…
          +1
          Вот тоже, периодически пытаюсь использовать array_filter или array_walk, но возникает вопрос, а foreach будет ли не нагляднее и не проще ли для понимания?
            +1
            Кстати, да, забыл сказать, что в том же PHP есть вполне нативный мапперы (array_map, array_walk, array_walk_recursive), фильтры (array_filter) и еще немеряно всяких вкусностей для массивов, реализованных на низком уровне и работающих быстро.
            Хочется такого себе синтаксического сахара? Да вот, пожалуйста. Всё есть.

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

            Вообще, в php так много функций обработок массивов и итераторов, как в наборе array_, так и в SPL, что городить еще что-то… Ну не вижу смsсла в упор.
              +2
              Нет, используйте array_filter когда нужно убрать ненужные элементы из массива или оставить только нужные, и используйте array_walk когда нужно изменить элементы данного массива, а foreach для итерирования нужен, а не для filter/reduce/etc. операций
                +1
                Мне это утверждение видится несколько голословным.
                Вы не могли бы привести хотя бы пару доводов в обоснование своей позиции?

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

                Лично я для себя нашел только одно удобное применение array_filter() — проверка на наличие непустых ячеек в в массиве:
                if (!array_filter($arr)) {
                  echo "Все ячейки пустые!"
                }
                
                  +1
                  Тут всего один довод — вы используете существующие функции по назначению. Я не знаю, насколько это будет правильно, но можно это назвать «семантично».

                  > Во-первых, как уже здесь упоминалось, foreach нагляден, привычен и читабелен.
                  foreach ($array as $k => $el) {
                      if ($el['smth'] > LIMIT) {
                          unset($array[$k]);
                      }
                  }

                  куда читабельней чем array_filter(), а привычки это дело наживное.

                  > Во-вторых, foreach позволяет совершенно естественно реализовать логику фильтрации любой сложности. В то время как для array_filter в любом мало-мальски сложном случае придется писать отдельную функцию.
                  Для этого в 5.3 ввели анонимные ф-ии, вот как выше приведенная логика будет выглядеть с array_filter:
                  $array = array_filter($array, function($el) {
                      return $el['smth'] <= LIMIT;
                  });
                    –4
                    Ой. В пыхе столько функций, что если использовать все — код писать будет некогда.
                    Есть реально удобные — http_build_query(), например.
                    Но когда функцию можно заменить простым циклом — я не вижу смысла вводить в код новую сущность.

                    Я лучше сам цикл заверну в функцию, и буду обращаться к ней. Это получится чище и понятнее, чем ваш анонимный уродец. Вы сами-то не видите разве, какой франкенштейн получился?

                    Я прекрасно понимаю программиста, которому хочется написать покороче. Но я, к сожалению, имею опыт не только написания, но и чтения кода. И я дам 10 тупых но читабельных копи-паст методов против одного завернутого хака с переподвыподвертом, экономящего аж две строчки кода.
                      –1
                      То что я хотел объяснить — когда я вижу array_filter — я понимаю, что тут мы отсекаем ненужные элементы, когда я вижу foreach — я понимаю только то, что мы будем итерироваться.
                      Да и раз уж программируете на этом языке — то стоит знать что он может и имеет. Да, тут 5к ф-ий, но для работы с массивами их не больше 30 и думаю о них стоит знать.

                      А то что тут написано… ну это логика велосипедостроения, вперед и с песней.
                        0
                        C array_filter я ещё могу согласиться, что это более очевидно, чем foreach, но что делать с array_walk, array_map и т.д.?

                        На данный момент я использую лишь array_filter, там где это возможно(и то осторожно), а вот все остальное…
                          +5
                          А мне вот array_map() нравится. Он тоже очень очевиден.
                          Например в таких случаях (сферический пример в вакууме):

                          $tags = explode(',', $_POST['tags']);
                          $tags = array_map('trim', $tags);
                            0
                            Класс — спасибо.
                            Такое использование действительно читается достаточно быстро, и главное оно меньше и элегантнее чем foreach!
                            +2
                            $method = function($name) {
                                return function($item) use($name) { return $item->$name(); };
                            };
                            
                            $itemsTitles = array_map($method('getTitle'), $items);
                            $itemsTexts  = array_map($method('getText'), $items);
                            $itemsDates  = array_map($method('getDate'), $items);
                            


                            vs.

                            $itemsTitles = array();
                            foreach ($items as $item) {
                                $itemsTitles[] = $item->getTitle();
                            }
                            $itemsTexts = array();
                            foreach ($items as $item) {
                                $itemsTexts[] = $item->getText();
                            }
                            $itemsDates = array();
                            foreach ($items as $item) {
                                $itemsDates[] = $item->getDate();
                            }
                            
                              +2
                              vs.

                              $items = to_collection($items);
                              
                              $itemsTitles = $items->mapBy(x()->getTitle());
                              $itemsTexts  = $items->mapBy(x()->getText());
                              $itemsDates  = $items->mapBy(x()->getDate());
                              
                                +4
                                Мне совсем не нравится эта бессмысленная фунция x(). Если уж ты начал делать DSL, основанный на функциях, то чего бы не идти до конца:

                                use collada\to_collection;
                                use collada\method;
                                
                                $items = array_to_coll($items);
                                
                                $itemsTitles = $items->mapBy(method('getTitle'));
                                $itemsTexts  = $items->mapBy(method('getText'));
                                $itemsDates  = $items->mapBy(method('getDate'));
                                
                                  0
                                  Увы, вообще не читабельно.
                                  +1
                                  Не знаю, не знаю.
                                  Я бы не сказал, что это прочитать проще, чем foreach, к тому же второй пример должен выглядеть вот так, что согласитесь, для понимания гораздо легче:
                                  $itemsTitles = array();
                                  $itemsTexts = array();
                                  $itemsDates = array();
                                  foreach ($items as $item) {
                                      $itemsTitles[] = $item->getTitle();
                                      $itemsTexts[] = $item->getText();
                                      $itemsDates[] = $item->getDate();
                                  }
                                  

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

                                    $itemsTitles = array_map($method('getTitle'), $items);
                                    // $itemsTitles = ARRAY OF $items, MAPPED BY method getTitle
                                    
                                    
                                    
                                    
                                    $itemsTitles = array();
                                    foreach ($items as $item) {
                                        $itemsTitles[] = $item->getTitle();
                                    }
                                    // $itemsTitles = ARRAY
                                    // for each $item in ARRAY OF $items
                                    // append to $itemsTitles $item->getTitle()
                                    
                                      0
                                      Во-втором случае мы используем строго возможности языка. Их ограниченное кол-во и их поведение мы знаем точно, так что на осмысление конструкции уйдет 1 «такт».

                                      В-первом случае, мы зависим от некого $method, о реализации которого, мы пока ничего не знаем. Т.е. нам придется туда погрузиться — 2 неких «такта»

                                      Если все разработчики точно знают что такое $method, соглашение используется во всем проекте, то возможно это и хорошо, в других случаях это усложнение со всеми вытекающими.
                                        0
                                        Ну вы только что подтвердили мои слова. Вы мыслите о коде в плоскости «тактов», я мыслю в плоскости «что я хочу получить». Весь смысл конструкций в функциональном стиле как раз в том, что вам не нужно видеть реализации, чтобы понять что делает $method('getTitle'). Вот серьезно, не притворяйтесь что можете прочитать эту конструкцию как-то по-другому нежели «метод getTitle».
                                          0
                                          Я не притворяюсь. У нас есть команда.
                                          Есть периодические смены состава. Бизнес трактует правило жизни проекта.
                                          Необходимо как можно быстрее вводить в строй программистов, что бы они с наименьшим кол-вом временя для адаптации входили в рабочий режим.

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

                                          Чем больше подобных $method появится, тем тяжелее сопровождать проект. А главное, тяжелее определиться объяснить команде, когда использовать foreach, а когда $method.
                                            0
                                            Извиняюсь за некоторые выражения(«Я не притворяюсь»), я сам не понял, как они попали в текст.
                          0
                          foreach позволяет совершенно естественно реализовать логику фильтрации любой сложности. В то время как для array_filter в любом мало-мальски сложном случае придется писать отдельную функцию.


                          Не вижу разницы, между кодом, который вы заключаете в блоке фигурных скобок в foreach и кодом внутри функции для array_filter — и там и там можно реализовать логику фильтрации любой сложности.
                    +2
                    Как описать x() в phpdoc (для автокомплита правильных геттеров)?
                      +2
                      Никак.
                      0
                      Странно, что в конкурентах не упомянут YaLinqo.
                        0
                        Забыл про эту библиотеку. Добавлю к сравнению.

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

                      Самое читаемое