Последняя функция, которую нам осталось рассмотреть из большой тройки функций высшего порядка - это reduce
. Эта функция более мощная, чем map
и filter
, и в конечном итоге они могут быть выражены через reduce
. По сути эта функция является базовой. Она очень мощная и позволяет решать большинство возникающих задач при работе по преобразованию структур.
Количество заголовков
import {
node, append, make
} from 'hexlet-html-tags';
const html1 = make();
const html2 = append(html1, node('h1', 'scheme'));
const html3 = append(html2, node('p', 'is a lisp'));
const html4 = append(html3, node('h1', 'haskell'));
const html5 = append(html4, node('p', 'pure'));
const html6 = append(html5, node('h2', 'php'));
headersCount('h1', html6); // 2
В этом примере решается задача подсчёта кол-ва заголовков. Вообще говоря, можно считать количество всего, чего угодно. Но в данном случае, это может быть полезно вот для чего.
Мы уже говорили, что в HTML заголовок h1
на странице может быть только один. Представьте, что мы строим валидатор, который проверяет наш HTML на качество и соответствие стандартам. Одной из проверок будет проверка на количество заголовков первого уровня. Если их больше одного, мы можем выдать предупреждение, что так делать нельзя.
В примере видно, что мы собираем HTML, в котором два заголовка h1
. Мы применяем функцию headersCount
, которая первым параметром принимает имя тега (можно было оставить только номер заголовка) и, собственно, HTML. В итоге в данной ситуации она выдаст значение 2
.
Внутри функция устроена так:
import {
l, isEmpty, is, head, tail
} from 'hexlet-pairs-data';
export const headersCount = (tagName, elements) => {
const iter = (items, acc) => {
if (isEmpty(items)) {
return acc;
}
const item = head(items);
const 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
реализует его из-за того, что явно используется аккумулятор.
Количество заголовков
import {
node, append, make, reduce
} from 'hexlet-html-tags';
const html1 = append(make(), node('h1', 'header1'));
const html2 = append(html1, node('h1', 'header2'));
const html3 = append(html2, node('p', 'content'));
reduce((element, acc) => {
return is('h1', element) ? acc + 1 : acc;
}, 0, html3); // 2
Давайте теперь рассмотрим использование редьюса как концепции и отдельной функции. Мы, как обычно, создаём HTML и дальше вызываем reduce
, который принимает на вход уже три элемента. Это сильно отличается от map
или filter
. Первый элемент — это функция, второй — аккумулятор. Он сильно зависит от того, какую операцию мы собираемся делать. Если бы нам понадобилось сформировать список, то начальным знанием был бы пустой список. Третьим параметром мы передаём сами элементы, которые будем обрабатывать.
Обратите внимание, что они всегда передаются последним параметром. Это сделано не просто так. Благодаря передаче этого аргумента можно сгенерировать несколько функций, встроить их в цепочку, а потом сквозь них протащить данные. Это использование так называемой функциональной композиции.
Та функция, которая непосредственно делает всю обработку в отличие фильтра и мапа принимает на вход два параметра. Первый — это текущий элемент, с которым мы работаем, а второй — это аккумулятор, потому что только мы знаем, как хотим его изменять. Всё, что делает внутренняя реализация переданной функции: мы проверяем, является ли текущий элемент заголовком h1
, и если да, то возвращаем аккумулятор, увеличенный на единицу, если нет, то возвращается сам аккумулятор.
В конечном итоге эта функция считает нам количество заголовков.
Ниже еще несколько примеров использования reduce
при работе со списками:
import { l, cons, reduce, toString, head } from '@hexlet/pairs-data';
const list = l(0, -10, 2, 38, 2, -2);
const list2 = reduce(Math.max, head(list), list);
console.log(toString(list2)); // => 38
const list3 = reduce((item, acc) => item + acc, 0, list);
console.log(toString(list3)); // => 30
https://repl.it/@hexlet/js-sequences-reduce
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.