Последняя функция, которую нам осталось рассмотреть из большой тройки функций высшего порядка - это reduce
. Эта функция более мощная, чем map
и filter
, и в конечном итоге они могут быть выражены через reduce
. По сути эта функция является базовой. Она очень мощная и позволяет решать большинство возникающих задач при работе по преобразованию структур.
Количество заголовков
<?php
use function Php\Html\Tags\HtmlTags\make;
use function Php\Html\Tags\HtmlTags\append;
use function Php\Html\Tags\HtmlTags\node;
$html1 = make();
$html2 = append($html1, node('h1', 'scheme'));
$html3 = append($html2, node('p', 'is a lisp'));
$html4 = append($html3, node('h1', 'haskell'));
$html5 = append($html4, node('p', 'pure'));
$html6 = append($html5, node('h2', 'php'));
headersCount('h1', $html6); // 2
В этом примере решается задача подсчёта количества заголовков. Вообще говоря, можно считать количество всего, чего угодно. Но в данном случае, это может быть полезно вот для чего.
Мы уже говорили, что в HTML заголовок h1
на странице может быть только один. Представьте, что мы строим валидатор, который проверяет наш HTML на качество и соответствие стандартам. Одной из проверок будет проверка на количество заголовков первого уровня. Если их больше одного, мы можем выдать предупреждение, что так делать нельзя.
В примере видно, что мы собираем HTML, в котором два заголовка h1
. Мы применяем функцию headersCount
, которая первым параметром принимает имя тега (можно было оставить только номер заголовка) и, собственно, HTML. В итоге в данной ситуации она выдаст значение 2
.
Внутри функция устроена так:
<?php
use function Php\Pairs\Data\Lists\l;
use function Php\Pairs\Data\Lists\isEmpty;
use function Php\Pairs\Data\Lists\is;
use function Php\Pairs\Data\Lists\head;
use function Php\Pairs\Data\Lists\tail;
function headersCount($tagName, $elements)
{
$iter = function ($items, $acc) use (&$iter) {
if (isEmpty($items)) {
return $acc;
}
$item = head($items);
$newAcc = is($tagName, $item) ? $acc + 1 : $acc;
return $iter(tail($items), $newAcc);
};
return $iter($elements, 0);
}
Поскольку она выдаёт какой-то конечный результат (скалярный), то понятно, что это другой тип преобразования. В отличие от map
и filter
нам нужно делать так называемую свёртку, когда все элементы перебираются, происходит общее вычисление, и в конце мы получаем новый результат, который не является отображением или фильтрацией предыдущего списка. Эту задачу можно решить с помощью итеративного процесса, используя внутреннюю функцию $iter
или цикл. Итак, давайте посмотрим, что здесь происходит.
Как обычно, при итеративном процессе мы вызываем $iter($elements, 0)
и передаём туда аккумулятор и наши элементы. Начальное значение аккумулятора равно 0
, потому что наша функция считает количество.
Дальше внутри функции $iter
всё происходит, как обычно. Мы берем "голову", вычисляем новый аккумулятор. Здесь есть проверка is
: соответствует ли переданное имя тега текущему элементу (в нашем случае, это заголовки). И если это так, то аккумулятор обновляется $acc + 1
. Если текущий элемент не является заголовком, то $newAcc
становится равным $acc
, т.е. не происходит никаких изменений.
После этого мы рекурсивно вызываем функцию $iter
, передавая в неё "хвост" — tail($items)
(остаток наших элементов) и новый аккумулятор. Таким образом итерация за итерацией аккумулятор накапливается и в самом конце, когда $items
окажется пустым, то наружу вернётся значение нашего аккумулятора.
Свёртка
Как видно данная операция может быть обобщена. Причём в этом случае обобщение может быть гораздо сильнее, чем отображение или фильтрация списков, потому что в данном случае reduce
может сгенерировать даже новый список, который может фактически быть отражением предыдущего списка, но при этом он может быть и не отражением: быть другим по размерам, содержать совершенно другие данные. Т.е. reduce
позволяет нам создавать за счёт аккумулятора нечто новое на основе базового списка.
Особенности
- Reduce может заменить
map
/filter
. - Обычно последняя функция в цепочке преобразований.
- Её часто называют fold.
- Внутренняя реализация возможна только через итеративный процесс.
reduce
может заменить map
/filter
, но придётся делать reverse
, чтобы элементы были возвращены в правильном порядке.
reduce
обычно последняя функция в цепочке преобразований. После map
и filter
в конечном итоге может применятся reduce
для получения какого-то результата. Это не всегда так, но чаще всего.
reduce
часто называют fold в зависимости от языка. В некоторых языках встречаются и другие названия. Например, в Ruby - это inject
.
Внутренняя реализация возможна только через итеративный процесс, потому что сам reduce
реализует его из-за того, что явно используется аккумулятор.
Количество заголовков
<?php
use function Php\Html\Tags\HtmlTags\make;
use function Php\Html\Tags\HtmlTags\append;
use function Php\Html\Tags\HtmlTags\node;
use function Php\Html\Tags\HtmlTags\reduce;
$html1 = append(make(), node('h1', 'header1'));
$html2 = append($html1, node('h1', 'header2'));
$html3 = append($html2, node('p', 'content'));
reduce($html3, fn($element, $acc) => is('h1', $element) ? $acc + 1 : $acc), 0) // 2
Давайте теперь рассмотрим использование редьюса как концепции и отдельной функции. Мы, как обычно, создаём HTML и дальше вызываем reduce
, который принимает на вход уже три элемента. Это сильно отличается от map
или filter
. Первый аргумент — это сами элементы, которые мы будем обрабатывать, второй — функция. Третьим параметром мы передаём аккумулятор. Он сильно зависит от того, какую операцию мы собираемся делать. Если бы нам понадобилось сформировать список, то начальным значением был бы пустой список.
Та функция, которая непосредственно делает всю обработку в отличие от фильтра и мапа принимает на вход два параметра. Первый — это текущий элемент, с которым мы работаем, а второй — это аккумулятор, потому что только мы знаем, как хотим его изменять. Всё, что делает внутренняя реализация переданной функции: мы проверяем, является ли текущий элемент заголовком h1
, и если да, то возвращаем аккумулятор, увеличенный на единицу, если нет, то возвращается сам аккумулятор.
В конечном итоге эта функция считает нам количество заголовков.
Ниже еще пример использования reduce
при работе со списками:
<?php
use function Php\Pairs\Data\Lists\l;
use function Php\Pairs\Data\Lists\toString;
use function Php\Pairs\Data\Lists\reduce;
$list = l(0, -10, 2, 38, 2, -2);
$list2 = reduce($list, fn($item, $acc) => $item + $acc, 0);
print_r(toString($list2)); // => 30
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.