Агрегирующие операции
Агрегирующая операция превращает всю коллекцию в одно значение. Примеры — количество элементов, их сумма, наибольший элемент или все элементы, объединённые в один результат.
Самые распространённые из них 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.
Вывод: PHP-скрипт
<?php
$numbers = [6, 42, 10, 4];
$sum = array_sum($numbers);
echo "Количество: ", count($numbers), "\n";
echo "Максимум: ", max($numbers), "\n";
echo "Минимум: ", min($numbers), "\n";
echo "Среднее: ", $sum / count($numbers), "\n";
echo "Сумма: ", $sum, "\n";
Минимум и максимум по правилу
Первый array_reduce() оставляет элемент с меньшим остатком % 3. Остатки чисел 5, 42, 10 и 4 — это 2, 0, 1 и 1, поэтому побеждает 42. Второй оставляет более длинную строку и находит "three". Начальное значение не задано, поэтому $best начинается с null, и проверка $best === null позволяет первому элементу победить в первом раунде. При равенстве остаётся элемент, который встретился раньше.
Вывод: PHP-скрипт
<?php
$numbers = [5, 42, 10, 4];
$words = ["one", "two", "three", "four"];
// Элемент, у которого остаток от деления на 3 наименьший.
// $best начинается с null, поэтому первый элемент всегда побеждает в первом раунде.
$minRem = array_reduce(
$numbers,
fn($best, $n) => ($best === null || $n % 3 < $best % 3) ? $n : $best
);
echo $minRem, "\n";
// Самое длинное слово, сравнение по длине.
$longest = array_reduce(
$words,
fn($best, $w) => ($best === null || strlen($w) > strlen($best)) ? $w : $best
);
echo $longest, "\n";
Сумма функции от каждого элемента
array_map() преобразует каждый элемент, array_sum() складывает результаты. Удвоение 5, 42, 10 и 4 даёт 122. Деление их на 2 даёт 30.5.
Вывод: PHP-скрипт
<?php
$numbers = [5, 42, 10, 4];
// Сначала преобразуем каждый элемент, затем складываем результаты.
echo array_sum(array_map(fn($n) => $n * 2, $numbers)), "\n";
echo array_sum(array_map(fn($n) => $n / 2, $numbers)), "\n";
Свёртка и редукция
Свёртка (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.
Вывод: PHP-скрипт
<?php
// reduce начинает с первого элемента и свёртывает в него остальные.
function reduce(array $items, callable $f): mixed {
return array_reduce(array_slice($items, 1), $f, $items[0]);
}
$numbers = [5, 2, 10, 4];
$sum = reduce($numbers, fn($sum, $element) => $sum + $element);
echo $sum, "\n";
Свёртка
array_reduce() с начальным значением 0 начинает с 0 и прибавляет каждый элемент, умноженный на 2. Удваивается каждый элемент: 10 + 4 + 20 + 8 = 42.
Вывод: PHP-скрипт
<?php
$numbers = [5, 2, 10, 4];
// Третий аргумент — начальное значение: удваивается каждый элемент.
$sumDoubled = array_reduce($numbers, fn($sum, $element) => $sum + $element * 2, 0);
echo $sumDoubled, "\n";
Свёртка против редукции
Оба вызова используют одну и ту же функцию: аккумулятор плюс элемент, умноженный на 2. reduce() даёт 37, потому что первый элемент 5 — это начало и он не удваивается. array_reduce() с начальным значением 0 даёт 42. Без этого 0 тоже получилось бы 42: началом было бы null, а null + 10 — это 10.
Вывод: PHP-скрипт
<?php
// reduce берёт первый элемент как начало, поэтому $f к нему никогда не применяется.
function reduce(array $items, callable $f): mixed {
return array_reduce(array_slice($items, 1), $f, $items[0]);
}
$addDoubled = fn($sum, $element) => $sum + $element * 2;
$numbers = [5, 2, 10, 4];
// Здесь 5 — начальное значение, поэтому оно не удваивается: 5 + 4 + 20 + 8
echo "Редукция: ", reduce($numbers, $addDoubled), "\n";
// Удваивается каждый элемент: 10 + 4 + 20 + 8
echo "Свёртка: ", array_reduce($numbers, $addDoubled, 0), "\n";
Свёртка справа
array_reverse($numbers) передаёт элементы в array_reduce() от последнего к первому. Сложению порядок не важен, поэтому результат снова 42.
Вывод: PHP-скрипт
<?php
$numbers = [5, 2, 10, 4];
// array_reverse() передаёт элементы от последнего к первому.
$sumDoubledRight = array_reduce(
array_reverse($numbers),
fn($sum, $element) => $sum + $element * 2,
0
);
echo $sumDoubledRight, "\n";
Свёртка с индексом
Иногда важна позиция элемента, а не только его значение.
Обратный вызов array_reduce() ключей не видит, а обычный цикл foreach — видит. foreach ($numbers as $idx => $element) идёт с начала. array_reverse($numbers, true) переворачивает массив, но сохраняет исходные ключи, потому что второй аргумент — true; поэтому foreach по нему идёт с конца и всё равно видит настоящий индекс каждого элемента. Внутри цикла if проверяет индекс и решает, прибавлять ли элемент.
Свёртка с индексом
Складываются только элементы на чётных индексах: на индексе 0 стоит 5, на индексе 2 — 10, поэтому сумма равна 15.
Вывод: PHP-скрипт
<?php
$numbers = [5, 2, 10, 4];
// foreach даёт ещё и индекс, поэтому условие может его использовать.
$sumEven = 0;
foreach ($numbers as $idx => $element) {
if ($idx % 2 === 0) {
$sumEven += $element;
}
}
echo $sumEven, "\n";
Свёртка с индексом справа
То же правило, но цикл начинается с индекса 3 и идёт вниз до 0. Результат всё тот же — 15.
Вывод: PHP-скрипт
<?php
$numbers = [5, 2, 10, 4];
// array_reverse(..., true) сохраняет исходные ключи: 3, 2, 1, 0.
$sumEven = 0;
foreach (array_reverse($numbers, true) as $idx => $element) {
if ($idx % 2 === 0) {
$sumEven += $element;
}
}
echo $sumEven, "\n";
Безопасная редукция
Редукции нужен первый элемент, с которого начать. На пустом массиве вспомогательная функция 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.
Вывод: PHP-скрипт
<?php
// reduceOrNull возвращает null вместо чтения отсутствующего $items[0].
function reduceOrNull(array $items, callable $f): mixed {
if ($items === []) {
return null;
}
return array_reduce(array_slice($items, 1), $f, $items[0]);
}
$add = fn($sum, $element) => $sum + $element;
$numbers = [5, 2, 10, 4];
$empty = [];
echo json_encode(reduceOrNull($numbers, $add)), "\n";
echo json_encode(reduceOrNull($empty, $add)), "\n";
// array_reduce() на пустом массиве возвращает своё начальное значение — здесь null.
echo json_encode(array_reduce($empty, $add)), "\n";
Промежуточные свёртка и редукция
Промежуточная (running) свёртка или редукция не выбрасывает промежуточные шаги. Она сохраняет аккумулятор после каждого элемента, так что результат — это массив всех промежуточных значений.
Встроенной функции для этого в PHP нет. Храните переменную-сумму и добавляйте её в массив результата через $result[] = $sum внутри цикла foreach. Для промежуточной редукции результат начинается с первого элемента. Для промежуточной свёртки результат начинается с начального значения и содержит на один элемент больше, чем входные данные.
Промежуточная редукция
Числа от 0 до 5 складываются по одному. Массив результата содержит все суммы по пути: 0, 1, 3, 6, 10, 15.
Вывод: PHP-скрипт
<?php
$numbers = [0, 1, 2, 3, 4, 5];
// Первый элемент — это начало; каждый следующий шаг сохраняется в результате.
$sum = $numbers[0];
$runningReduceSum = [$sum];
foreach (array_slice($numbers, 1) as $item) {
$sum += $item;
$runningReduceSum[] = $sum;
}
echo implode(", ", $runningReduceSum), "\n";
Промежуточная свёртка
То же самое с начальным значением 10. Результат начинается с самой 10 и содержит на один элемент больше, чем входные данные: 10, 10, 11, 13, 16, 20, 25.
Вывод: PHP-скрипт
<?php
$numbers = [0, 1, 2, 3, 4, 5];
// Начальное значение 10 — это первый элемент результата.
$sum = 10;
$runningFoldSum = [$sum];
foreach ($numbers as $item) {
$sum += $item;
$runningFoldSum[] = $sum;
}
echo implode(", ", $runningFoldSum), "\n";