Привет.

Я бы хотел представить мой первый open source проект, размещённый на github. Это библиотека предоставляющая удобный и совершенно новый опыт работы с числовыми массивами в php. Вдохновением для создания послужила библиотека numoy на языке Python, которая включает широкий спектр возможностей для манипулирования данными и инструментами линейной алгебры.

Честно говоря, я был удивлён, когда обнаружил что на php до сих пор нет достойного аналога numpy. Конечно, некоторые попытки реализации можно найти на гитхабе, но они все крайне далеки от оригинала и не разделяют ту же идеологию. Моей целью было и есть создание похожей библиотеки, как минимум в отношении удобства использования и краткости синтаксиса. В дальнейших планах будет улучшение производительности.

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

$list = [16, 22, -6, 23, -1, 13, 24, -23, 22];

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


$result = [];
for($i=0; $i<count($list); $i++)
    if ($list[$i] >= 0)
        $result[] = $list[$i];

В результате получим массив из 6 элементов. Но, для такой простой и тривиальной операции мы напечатали слишком много кода. Конечно, можно использовать краткие версии вроде array_walk или аналогичные, но, всё равно будет много явной логики.

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


// cast our array to the numphp array 
$list = new np_array($list);
$result = $list[$list['> 0']];

Вот так просто!

Более того, это только лишь начало. Вы можете манипулировать данным объектом как захотите и даже устанавливать значения по определённому условию.


$list[$list['< 0']] = 0;
//result
[16, 22, 0, 23, 0, 13, 24, 0, 22]

Но постойте. До сих пор мы говорили только про выборку и изменение данных. А как насчёт математических операций? Хороший вопрос.

Давайте представим, вы создаёте RPG игру и у вашего героя есть какие-то способности, представленные в таком формате:


$powers = [62, 88, 34, 29];

Теперь, во время повышения уровня, вы хотите увеличить каждую способность на определённый показатель. Опять же, как бы вы поступили без numphp?


for($i=0; $i<count($powers); $i++)
    $powers[$i]++;

Этот код работает, но разве он выглядит восхитительно? Вам нужно писать эти циклы раз за разом. Сравните это с элегантным решением библиотеки numphp:


$powers = $powers->add(1);

Более того, вы можете объединить предыдущие две возможности и, скажем, увеличить только те способности, которые на данный момент имеют значение меньше 30:


$powers = $powers[$powers['< 30']]->add(1);

Другая крутая возможность состоит в том, что вы можете выполнять математические операции и операции сравнения между двумя массивами всё в том же простом и понятном синтаксисе:


$powers = [62, 88, 34, 29];
$intensify = [2, 5, 4, 1];
$result = $powers->add($intensify);
//result
[64, 93, 38, 30]

Как вы можете видеть, numphp предоставляет элегантный синтаксический сахар для рутинных операций с числовыми массивами. Я описал лишь главную идею и возможности библиотеки на данный момент. Дополнительно уже реализованы удобные хелперы вроде генераторов (массив нулей, единиц, диапазона и даже ряда Фибоначчи), random модуль и так далее.

Обратите внимание, библиотека находится только в самом начале своего развития и я планирую добавлять новые возможности. Самая главная из них на данный момент — возможность работы с n-мерными массивами (матрицами, к примеру) и выполнять все основные виды операций из линейной алгебры.

Дополнительные возможности и документацию вы можете найти здесь.

Также, если вы хотите внести свой вклад в развитие — буду рад обсудить любые вопросы.

Комментарии (56)


  1. masterx
    24.01.2018 22:45

    Перефразируя классика, можно сказать, что все php–библиотеки нужны, все php–библиотеки важны. А если серьезно, то библиотека наверняка может быть полезна для повторяющихся специфичных задач с массивами чисел. Для единичных же случаев чаще всего можно обойтись без циклов, но с map, filter, reduce.


    1. apollonin Автор
      24.01.2018 22:56

      Совершенно верно. Особенно важно учитывать что в задачах связанных с data mining, machine learning, deep learning, такие вещи повторяюсь из раза в раз и тратить время рутинные циклические операции не стоит вообще


  1. Gemorroj
    24.01.2018 22:55

    1. apollonin Автор
      24.01.2018 22:57

      да, спасибо большое. Это имеет место быть. В последнее время пишу на python и там другие соглашения и глаз уже радуется python-style, а php-style не нравится. НО раз это php либа — нужно следовать psr )


  1. Lure_of_Chaos
    24.01.2018 23:36

    > $list[$list['> 0']];
    о нет, только не вуду-магия (по всей видимости, с парсингом), уж лучше filter по предикату


    1. apollonin Автор
      25.01.2018 00:29

      если настолько сильно не нравится вуду-магия, можете использовать явный синтаксис =)

      $result = $list[$list->gt(25)];


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


      1. masterx
        25.01.2018 01:22

        Если мы о «читаемом», тогда уж сделайте

        $result = $list->gt(25); // [26, 78, 99]


        1. apollonin Автор
          25.01.2018 01:47

          Конечно, такой вариант я рассматривал.но:

          1. это противоречит подходу, реализованному в numpy библиотеки и этому есть веская причина:
          2. $list->gt(25) вернёт массив, называемый булевой маской: [false, false, true, true, false, true], где false — элемент в этой позиции НЕ удовлетворяет условию, true — удовлетворяет.

          Преимущество такого подхода заключается в том, что такие булевые маски позволяют очень гибко выбрать элементы из массив и в дополнение их можно комбинировать и делать их логическое объединение:

          $result = $list[operator::b_and($list->gte(5), $list->lt(8))];
          
          // or
          
          $result = $list[operator::b_and($list['>= 5'], $list['< 8'])];
          


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

          $list[$list > 25];
          
          // and
          
          $result = $list[($list >= 5) & ($list < 8)];
          


          1. Fesor
            25.01.2018 13:05

            не достигается ли этот же вариант битовых масок обычной композицией функций?


            function gt($b) {
                return function ($a) use ($b) {
                    return $a > $b;
                };
            }
            function lt($b) {
                return function ($a) use ($b) {
                    return $a < $b;
                };
            }
            
            function all(\Closure ...$filters) {
                return function (...$args) use ($filters) {
                    foreach ($filters as $filter) {
                        if (!$filter(...$args)) {
                            return false;
                        }
                    }
                    return true;
                };
            }
            
            function any(\Closure ...$filters) {
                return function (...$args) use ($filters) {
                    foreach ($filters as $filter) {
                        if ($filter(...$args)) {
                            return true;
                        }
                    }
                    return false;
                };
            }
            
            $arr = range(0, 40);
            array_filter($arr, any(lt(2), all(gt(25), lt(30)))); // 0, 1, 26, 27, 28, 29

            может быть я не учитываю каких-то юзкейсов конечно… ну и да, понятно что php для подобного пока не самый удобный язык.


            1. apollonin Автор
              25.01.2018 13:35

              по сути, часть библиотеки и представляет собой отдельные методы-хелперы, которые потом можно компоновать как угодно, так как они возвращают одинаковые типы данных: np_array в данным случае.
              методы all, any, кстати, в ближайших планах тоже ;)


            1. apollonin Автор
              25.01.2018 13:49

              да, ещё. булевые маски полезны в случае если нужно выбрать более гибко по индексам. К примеру из массива из 10 элементов выбрать только 1ый, 3-ий, 6-ой и 9-ый элементы.

              $list[[false, true, false, true, false, false, true, false, false, true, false]];


              1. Fesor
                25.01.2018 14:59

                К примеру из массива из 10 элементов выбрать только 1ый, 3-ий, 6-ой и 9-ый элементы.

                в php это ограничение array_filter которое не предоставляет доступа к ключам. В JS том же я вполне могу сделать так:


                arr.filter(byKey(in(1, 3, 6, 9))


                1. SerafimArts
                  25.01.2018 15:33

                  в php это ограничение array_filter которое не предоставляет доступа к ключам.

                  Чуток не понял. Коллбек array_filter принимает как ключи, так и значения. О каких ограничениях речь?


                  1. Fesor
                    25.01.2018 21:01

                    да но согласись, с флагом это будет не так удобно, хотя думаю всеравно нужна будет свое обертка.


      1. zzzmmtt
        25.01.2018 09:28

        $resultArrray = $sourceArray[$something];

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


        1. apollonin Автор
          25.01.2018 13:46

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


          1. oxidmod
            25.01.2018 13:56

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


      1. Kicker
        25.01.2018 10:07

        О да, стало намного читабельнее…


      1. Fesor
        25.01.2018 12:55

        эх был бы пайп оператор в пыхе...


        use function myCooolLib\Comparsion\gt;
        use function \array_filter as filter
        
        $arr = range(10, 30)
         |> filter($$, gt(25)); // 26, 27, 28, 29, 30

        а вообще есть интересная RFC на эту тему: operator functions. Но вот только я бы для начала пофиксил способ задания ссылок на функции.


        1. apollonin Автор
          25.01.2018 13:40

          мне больше бы импонировао, если бы приняли вот это: wiki.php.net/rfc/operator-overloading


        1. SerafimArts
          25.01.2018 14:51

          PIPE оператор можно реализовать через HOM прокси (типа такого) с доступом к глобальным функциям. Получаем:

          use function \array_filter as filter
          
          wrapper(range(10, 30))
              ->filter(_, gt(25));
          


  1. genew
    25.01.2018 07:23

    Вместо кода

    $result = [];
    for($i=0; $i<count($list); $i++)
        if ($list[$i] >= 0)
            $result[] = $list[$i];

    можно написать так:
    $result = array_filter($list, function($v) { return $v >= 0; });


    1. apollonin Автор
      25.01.2018 13:41

      можно. но при работе в области data science, machine learning такие операции нужно делать по 100 раз в день с разными условиями и так далее. Массажирование данных, так сказать.


  1. oxidmod
    25.01.2018 09:33

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

    $result = $list[$list['> 0']];

    если бы объявление было где — то выше.


    1. apollonin Автор
      25.01.2018 13:43

      Могу согласиться с тем, что такой синтаксис непривычен. Но тут, как всегда, дело контекста.
      если вы работаете в проекте где все знают что используется либа numphp и мы с ней знакомы, то проблем не будет. Более того, IDE подсветит что это объект.
      Опять же, если кому крайне непривычно, можно использовать явный синтаксис: $list->gt(0);


      1. oxidmod
        25.01.2018 13:58

        Каждый раз нужно выходить из потока, чтобы понять перед тобой обычный массив или объект либы. Особенно, когда '> 0' приходит в виде аргумента. Да, это проще когда постоянно работаешь с проектом, но в целом все-же не ок.


        1. apollonin Автор
          25.01.2018 14:05

          В любом случае, $list['>0'] — малая часть возможностей библиотеки =)
          Математические операции над векторами — частая задача.
          В ближайших планах — быстрый и удобный способ получить среднее значение, медиану или сумму всех элементов. $list->mean() и погнал дальше.


  1. AlexLeonov
    25.01.2018 11:20

    for($i=0; $i<count($list); $i++)
    Месье не слышал о цикле foreach?
    Я за такое студентам сразу «кол» ставлю. Рекомендую повторять вслух, как мантру, до полного просветления: «индексы массивов в PHP не обязаны быть плотны, монотонны и вообще не обязаны быть числами»


    1. zzzmmtt
      25.01.2018 14:01

      foreach, равно как и прочие циклы не всегда хороши при обходе массива. Есть же array_walk, array_filter и иже с ними.


      1. AlexLeonov
        25.01.2018 14:30

        Если сравнивать foreach и for в PHP — вы что предпочтете?


        1. zzzmmtt
          25.01.2018 14:42

          Зависит от исходных данных и условий. Для простого прямого обхода массива foreach однозначно проще и удобнее. Для индексированного числами в строгом порядке массива, для выборки элементов с определенным шагом — почему бы не использовать for?


          1. AlexLeonov
            25.01.2018 14:59

            Почему? Потому что в языке нет инструмента, гарантирующего, что ваш массив так и останется «индексированным числами в строгом порядке»

            for для обхода массивов — логическая бомба, которую вы подкладываете в свой код


            1. SerafimArts
              25.01.2018 15:27

              Потому что в языке нет инструмента, гарантирующего, что ваш массив так и останется «индексированным числами в строгом порядке»


              Почему же? Для массивов в пыхе есть массивы, а не только хеш-мапы.


              1. AlexLeonov
                25.01.2018 17:53

                SplFixedArray не гарантирует плотность индексов.


                1. SerafimArts
                  25.01.2018 18:18

                  Почему?

                  Заголовок спойлера
                  $s = new \SplFixedArray(10);
                  $s[0] = 23;
                  $s[2] = 42;
                  
                  for ($i = 0, $len = \count($s); $i < $len; ++$i) {
                      echo \gettype($s[$i]) . "\n";
                  }
                  
                  /**
                  integer
                  NULL
                  integer
                  NULL
                  NULL
                  NULL
                  NULL
                  NULL
                  NULL
                  NULL
                  **/
                  


                  1. Fortop
                    25.01.2018 23:39

                    Потому.

                    <?php
                    $splf = SplFixedArray::fromArray([3 => 33,22,4 => 44]);
                    var_dump($splf);
                    


        1. zzzmmtt
          25.01.2018 14:54

          Каждой задаче — свой инструмент. Ставить «кол» студентам за использование for — не логично. Логично уточнить почему был выбран for|foreach|while|array_walk|etc.


          1. AlexLeonov
            25.01.2018 15:00

            Отчего же. Совершенно логично.
            Цикл for не проходится намеренно. Если он встречается в ДЗ, это значит, что ДЗ было списано, причем было списано совершенно бездумно, поскольку на лекции несколько раз повторяется, почему нужно использовать именно foreach


            1. oxidmod
              25.01.2018 15:09

              Как использовать foreach для выбора каждого сотого элемента из массива в несколько сотен тысяч элементов?


              1. AlexLeonov
                25.01.2018 15:27

                Вопрос не имеет отношения к теме ветки дискуссии.
                Ровно также «никак», как и цикл for.


              1. SerafimArts
                25.01.2018 15:28

                Имеется ввиду это?

                foreach($items as $i => $value) {
                    if ($i % 100 === 0) { yield $value; }
                }


                1. apollonin Автор
                  25.01.2018 15:35

                  Это похоже не дело принципа =) обход массива с заданным шагом — классическая задача for массива. это значительно более читабельно чем $i % 100. более того, foreach будет проходить и сравнивать каждый элемент, хотя это не оптимально, а for не тратит время на это, более того, не создаёт $value переменную каждый раз, тратя на это ресурсы.


                  1. oxidmod
                    25.01.2018 15:42

                    Именно. Целью моего коммента было оспорить безапелляционность заявления

                    Если он встречается в ДЗ, это значит, что ДЗ было списано, причем было списано совершенно бездумно, поскольку на лекции несколько раз повторяется, почему нужно использовать именно foreach


                    1. AlexLeonov
                      25.01.2018 17:25

                      А с чем тут спорить-то? Я совершенно точно знаю, что если встретил for для обхода массива — это говнокод, который подсказали студенту где-нибудь на «Ответах Mail.ru», поскольку оператор for принципиально не озвучивается на лекциях.
                      В чем предмет спора?


                      1. oxidmod
                        25.01.2018 17:29

                        В том, что вы неправы. Ниже человек даже бенчмарк провел.
                        for это такой же оператор как и другие. Каждому есть свое применение (даже eval-у и goto). Просто нужно понимать, что ты делаешь, вот и все.
                        То, что вы ставите свои принципы выше объективной истины, не делает вам чести, как преподавателю.

                        зы. Если студент использовал что-то, что не входило программу и может обосновать почему он это сделал, то это бонус студенту.


                        1. AlexLeonov
                          25.01.2018 17:41

                          В чем я неправ? В том, что в общем случае цикл for неприменим для перебора элементов массивов в PHP?
                          Вы это серьезно?

                          $days = [
                          'jan' => 31,
                          'feb' => 28,
                          'mar' => 31,
                          ];

                          — примените здесь цикл for, пожалуйста. Хотя бы для того, чтобы подсчитать сумму количества дней во всех месяцах.
                          И тогда я немедленно признаю, что был неправ.


                          1. oxidmod
                            25.01.2018 18:02

                            Вы глупый или притворяетесь?

                            Еще раз процитирую свой коммент:

                            for это такой же оператор как и другие. Каждому есть свое применение (даже eval-у и goto).


                            Чтобы посчитать сумму элементов массива не нужен цикл совсем. Если понимать, что делаешь, то напишешь просто
                            $sum = array_sum($days);


                            for лучше подходит для перебора элементов массива с шагом. Выбор каждого k-того элемента, к примеру. И чем больше длина массива и шаг, тем for эффективней.

                            for лучше подходит для перебора массива в обратном порядке.

                            Да, for требует целочисленных индексов без пропусков. Если ты это понимаешь и учитываешь, то глупо отбрасывать возможность языка.


                          1. michael_vostrikov
                            25.01.2018 19:16

                            примените здесь цикл for, пожалуйста

                            Пожалуйста, цикл for:


                            for ($sum = 0, $i = 0, $values = array_values($days), $cnt = count($values); $i < $cnt; $i++)
                                $sum += $values[$i];


                      1. michael_vostrikov
                        25.01.2018 19:09

                        По-вашему, студент не может нигде получить информацию кроме ваших лекций? Или может вы считаете, что до ваших лекций ни один студент не мог изучать другой язык. где есть for?


                  1. SerafimArts
                    25.01.2018 17:09

                    1) $value не создаётся каждый раз. Напомню, что выделение zval структуры происходит единожды и при следующем «тике» итерации, если этот контейнер не был задействован, то он переиспользуется. За счёт этого оптимизируются тяжёлые операции аллоцирования памяти. Помимо этого использование корутинки однозначно указывает на то, что в памяти может находиться только один контейнер для $value в один момент времени.
                    2) Получение элемента по индексу хеш-мапы — это, если ничего не путаю, операция O(n), а вызов next() итератора — это O(1). По-этому, в сумме, цикл for будет жрать O(log n), а foreach по всем элементам O(n). Кажется, я ничего не перепутал =)
                    3) Ну и тривиальный косяк с тем, кто в хеш-мапе (массиве пыха) индексы не упорядочены, а значит надо либо итерироваться по внутреннему итератору «массива» (функции next), либо делать slice, либо затратить тот же самый O(n) для сброса ключей через array_value

                    Кажется всё логично? Написал свои пунктики выше, проверил специально, и… Нет, нифига, for всё равно быстрее. Расскажете где я ошибся в своих выводах? А то, признаться, хз.

                    Заголовок спойлера
                    // foreach
                    function a(iterable $items): iterable
                    {
                        foreach ($items as $i => $value) {
                            if ($i % 100 === 0) {
                                yield $value;
                            }
                        }
                    }
                    
                    // for
                    function b(iterable $items): iterable
                    {
                        $array = \array_values(\is_array($items) ? $items : \iterator_to_array($items));
                    
                        for ($i = 0, $len = \count($array); $i < $len; $i += 100) {
                            yield $array[$i];
                        }
                    }
                    
                    $items  = \range(10, 100000);
                    
                    // foreach tests (~0.050s)
                    $before = \microtime(true);
                    foreach (a($items) as $i) {
                        \ob_start();
                        \var_dump($i);
                        \ob_end_clean();
                    }
                    echo \number_format(\microtime(true) - $before, 5) . 's' . "\n";
                    
                    
                    // for tests (~0.005s)
                    $before = \microtime(true);
                    foreach (b($items) as $i) {
                        \ob_start();
                        \var_dump($i);
                        \ob_end_clean();
                    }
                    echo \number_format(\microtime(true) - $before, 5) . 's' . "\n";
                    


                    P.S. буферизацию добил, дабы случайно на DCE оптимизацию не нарваться.


                    1. oxidmod
                      25.01.2018 17:26

                      Доступ по индексу всегда O(1)

                      PHP arrays can contain integer and string keys at the same time as PHP does not distinguish between indexed and associative arrays.


                      Но да, индексы нужно держать в уме


                      1. oxidmod
                        25.01.2018 17:32

                        Не успел дописать. Количество итераций разное. И чем больше шаг и длинна исходного массива, тем больше разница в количестве итераций и сравнений.


                      1. SerafimArts
                        25.01.2018 18:15

                        Ну так в вашей цитате как раз и написано, что это всё хешмапы, а к ним доступ O(n). Откуда O(1)?


                        1. oxidmod
                          25.01.2018 19:14

                          Оттуда, что это хешмапа.
                          Сложность всегда одинакова, независимо от размера (опустим колизии хеш-функции)

                          Алгоритм всегда один и тот же:
                          1. Посчитали хеш от индекса
                          2. Обратились по полученному хешу.

                          O(n) было бы, если бы мы перебирали все елементы пока не найдем нужный


                          1. SerafimArts
                            26.01.2018 02:37

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


  1. dom1n1k
    25.01.2018 13:52

    Ну как экспериментально-учебно-пет-проект пойдёт.
    Но на практике — не взлетит. Не видно ни одного преимущества перед стандартными filter/map/etc — ни по удобству синтаксиса, ни по быстродействию.