Перейти к основному содержимому

Агрегирующие операции

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

Самые распространённые из них PHP предоставляет как встроенные функции. count() считает элементы, min() и max() находят наименьший и наибольший элемент, а array_sum() складывает их.

Будьте внимательны: min() и max() на пустом массиве выбрасывают ValueError («must contain at least one element»), и программа останавливается, если вы его не перехватите. Когда массив может быть пустым, сначала проверьте count($s) > 0. А array_sum([]) — это просто 0.

Для поэлементного объединения есть array_reduce($items, $callback, $initial). Она хранит промежуточное значение — аккумулятор — и по очереди вызывает вашу функцию с аккумулятором и очередным элементом. Аккумулятор начинается со значения $initial, а если $initial не указать, он начинается с null, а не с первого элемента. На этой странице показано, что это значит и как получить оба способа объединения, которые обычно называют свёрткой (fold) и редукцией (reduce).


Основные агрегаты

Самые частые агрегаты — количество, минимум, максимум, сумма и среднее.

  • count($numbers) даёт количество.
  • min($numbers) и max($numbers) дают наименьший и наибольший элемент. Они работают и с числами, и со строками.
  • array_sum($numbers) складывает все элементы. Среднее — это сумма, делённая на количество. Оператор / в PHP сохраняет дробную часть, когда деление не нацело, поэтому 62 / 4 — это 15.5.

Иногда наименьший или наибольший элемент нужно выбирать по правилу, а не по самому значению. min() и max() правила не принимают, зато array_reduce() может хранить лучший элемент: на каждом шаге сравнивать лучший элемент из найденных с текущим и оставлять победителя.

Иногда каждый элемент нужно сначала преобразовать функцией, а результаты сложить. Соедините две встроенные функции: array_map() преобразует, array_sum() складывает.

Количество, минимум, максимум, сумма и среднее

Для чисел 6, 42, 10 и 4 выведите количество с помощью count(), наибольший и наименьший элемент с помощью max() и min(), а сумму с помощью array_sum(). Среднее равно 62 / 4 = 15.5.

Prompt
Для чисел 6, 42, 10 и 4 выведите количество с помощью count(), наибольший и наименьший элемент с помощью max() и min(), среднее как сумму, делённую на количество, и сумму с помощью array_sum(). Снабдите каждую строку подписью вида «Count:», «Max:» и так далее.
Вывод: PHP-скрипт
Count, min, max, sum and average

Минимум и максимум по правилу

Первый array_reduce() оставляет элемент с меньшим остатком % 3. Остатки чисел 5, 42, 10 и 4 — это 2, 0, 1 и 1, поэтому побеждает 42. Второй оставляет более длинную строку и находит "three". Начальное значение не задано, поэтому $best начинается с null, и проверка $best === null позволяет первому элементу победить в первом раунде. При равенстве остаётся элемент, который встретился раньше.

Prompt
Для чисел 5, 42, 10 и 4 найдите элемент, у которого остаток от деления на 3 наименьший, с помощью array_reduce, который сохраняет лучший элемент (начните с null). Затем тем же способом найдите самое длинное из слов one, two, three, four. Выведите оба результата.
Вывод: PHP-скрипт
Min and max by a rule

Сумма функции от каждого элемента

array_map() преобразует каждый элемент, array_sum() складывает результаты. Удвоение 5, 42, 10 и 4 даёт 122. Деление их на 2 даёт 30.5.

Prompt
Для чисел 5, 42, 10 и 4 выведите сумму всех элементов, умноженных на 2, а затем сумму всех элементов, делённых на 2, каждый раз используя array_sum по array_map.
Вывод: PHP-скрипт
Sum of a function of each element

Свёртка и редукция

Свёртка (fold) и редукция (reduce) шаг за шагом объединяют все элементы в одно значение. Они хранят промежуточное значение, которое называется аккумулятором. Для каждого элемента они вызывают функцию с аккумулятором и элементом, а функция возвращает новый аккумулятор.

Разница — в начальном значении:

  • Свёртка начинается с начального значения, которое вы ей задаёте. Функция выполняется по одному разу для каждого элемента. В PHP это в точности array_reduce($items, $f, $initial).
  • Редукция начинается с первого элемента. Функция выполняется начиная со второго элемента, поэтому первый элемент через неё никогда не проходит.

Несмотря на название, array_reduce() — это всегда свёртка. Без третьего аргумента она начинает с null, а в арифметике null считается за 0 — так что функция всё равно выполняется для каждого элемента. Настоящая редукция берёт первый элемент как начало и свёртывает остальные: array_reduce(array_slice($items, 1), $f, $items[0]). Небольшая вспомогательная функция reduce() оборачивает эту строку.

Это важно, когда функция изменяет элемент. Если она удваивает каждый элемент, редукция оставляет первый элемент как есть.

В PHP нет редукции справа. Чтобы свернуть справа, передайте array_reduce() перевёрнутый массив от array_reverse(): элементы придут от последнего к первому, а функция всё так же получит первым аккумулятор, а вторым элемент.

Редукция

Вспомогательная функция reduce() берёт 5 как начало и свёртывает в него 2, 10 и 4. Результат — 21.

Prompt
Напишите вспомогательную функцию reduce, которая начинает с первого элемента массива и объединяет с ним остальные через array_reduce по array_slice($items, 1) с начальным значением $items[0]. С её помощью сложите числа 5, 2, 10 и 4 и выведите результат.
Вывод: PHP-скрипт
Reduce

Свёртка

array_reduce() с начальным значением 0 начинает с 0 и прибавляет каждый элемент, умноженный на 2. Удваивается каждый элемент: 10 + 4 + 20 + 8 = 42.

Prompt
С помощью array_reduce с начальным значением 0 прибавьте к аккумулятору каждое из чисел 5, 2, 10 и 4, умноженное на 2. Выведите результат.
Вывод: PHP-скрипт
Fold

Свёртка против редукции

Оба вызова используют одну и ту же функцию: аккумулятор плюс элемент, умноженный на 2. reduce() даёт 37, потому что первый элемент 5 — это начало и он не удваивается. array_reduce() с начальным значением 0 даёт 42. Без этого 0 тоже получилось бы 42: началом было бы null, а null + 10 — это 10.

Prompt
Напишите вспомогательную функцию reduce, которая начинает с первого элемента (array_reduce по array_slice($items, 1) с $items[0] в качестве начального значения). Определите функцию $addDoubled, которая возвращает $acc + $element * 2. Примените её к числам 5, 2, 10 и 4 один раз через reduce() и один раз через array_reduce с начальным значением 0 и выведите оба результата с подписями «Reduce:» и «Fold:».
Вывод: PHP-скрипт
Fold versus Reduce

Свёртка справа

array_reverse($numbers) передаёт элементы в array_reduce() от последнего к первому. Сложению порядок не важен, поэтому результат снова 42.

Prompt
С помощью array_reduce с начальным значением 0 по array_reverse($numbers) сложите каждое из чисел 5, 2, 10 и 4, умноженное на 2, двигаясь от последнего элемента к первому. Выведите результат.
Вывод: PHP-скрипт
Fold from the right

Свёртка с индексом

Иногда важна позиция элемента, а не только его значение.

Обратный вызов array_reduce() ключей не видит, а обычный цикл foreach — видит. foreach ($numbers as $idx => $element) идёт с начала. array_reverse($numbers, true) переворачивает массив, но сохраняет исходные ключи, потому что второй аргумент — true; поэтому foreach по нему идёт с конца и всё равно видит настоящий индекс каждого элемента. Внутри цикла if проверяет индекс и решает, прибавлять ли элемент.

Свёртка с индексом

Складываются только элементы на чётных индексах: на индексе 0 стоит 5, на индексе 2 — 10, поэтому сумма равна 15.

Prompt
Сложите только те элементы из 5, 2, 10, 4, которые стоят на чётном индексе. Используйте цикл foreach с индексом и условие if по индексу. Выведите сумму.
Вывод: PHP-скрипт
Fold with index

Свёртка с индексом справа

То же правило, но цикл начинается с индекса 3 и идёт вниз до 0. Результат всё тот же — 15.

Prompt
Сложите только те элементы из 5, 2, 10, 4, которые стоят на чётном индексе, двигаясь от последнего индекса к 0 с помощью foreach по array_reverse($numbers, true), который сохраняет исходные ключи. Выведите сумму.
Вывод: PHP-скрипт
Fold with index from the right

Безопасная редукция

Редукции нужен первый элемент, с которого начать. На пустом массиве вспомогательная функция reduce() прочитала бы $items[0], которого не существует: PHP выведет предупреждение («Undefined array key 0») и продолжит со значением null.

В PHP «результата нет» принято выражать через null. Небольшая вспомогательная функция сначала проверяет массив: если он пуст, она возвращает null, не обращаясь к $items[0]. Вызывающий код затем проверяет результат на null — точно так же, как $map[$key] ?? $default обрабатывает отсутствующий ключ. Сама array_reduce() уже ведёт себя так: на пустом массиве она возвращает своё начальное значение, а это null, если вы его не задали.

Редукция или null

reduceOrNull() возвращает 21 для чисел 5, 2, 10 и 4 и null для пустого массива. Последняя строка показывает, что array_reduce() без начального значения тоже возвращает null для пустого массива. json_encode() выводит null как слово null.

Prompt
Напишите функцию reduceOrNull, которая возвращает null для пустого массива, а иначе начинает с первого элемента и свёртывает в него остальные с помощью array_reduce. Вызовите её с функцией сложения на числах 5, 2, 10 и 4 и на пустом массиве. Затем вызовите array_reduce на пустом массиве без начального значения. Выведите все три результата с помощью json_encode.
Вывод: PHP-скрипт
Reduce or null

Промежуточные свёртка и редукция

Промежуточная (running) свёртка или редукция не выбрасывает промежуточные шаги. Она сохраняет аккумулятор после каждого элемента, так что результат — это массив всех промежуточных значений.

Встроенной функции для этого в PHP нет. Храните переменную-сумму и добавляйте её в массив результата через $result[] = $sum внутри цикла foreach. Для промежуточной редукции результат начинается с первого элемента. Для промежуточной свёртки результат начинается с начального значения и содержит на один элемент больше, чем входные данные.

Промежуточная редукция

Числа от 0 до 5 складываются по одному. Массив результата содержит все суммы по пути: 0, 1, 3, 6, 10, 15.

Prompt
Для чисел от 0 до 5 начните сумму с первого элемента и прибавляйте остальные по одному в цикле foreach, сохраняя каждую промежуточную сумму в массив результата, который начинается с первого элемента. Выведите результат с помощью implode.
Вывод: PHP-скрипт
Running reduce

Промежуточная свёртка

То же самое с начальным значением 10. Результат начинается с самой 10 и содержит на один элемент больше, чем входные данные: 10, 10, 11, 13, 16, 20, 25.

Prompt
Для чисел от 0 до 5 начните сумму с начального значения 10 и прибавляйте каждый элемент по одному в цикле foreach, сохраняя каждую промежуточную сумму в массив результата, который начинается с 10. Выведите результат с помощью implode.
Вывод: PHP-скрипт
Running fold