Создание цепочек из функциональных инструментов #
Васильев Андрей Михайлович, 2022
Версии презентации
Содержание #
- Способы формирования цепочек из функциональных инструментов, чтобы выполнять сложные запросы к данным
- Варианты замены сложных существующих циклов с помощью цепочек функциональных инструментов
- Построение конвейеров по изменению данных для выполнения работы
Мы изучили базовые инструменты функционального итерирования по массивам. Однако реальные задачи зачастую сложнее и требуют применения сразу набора из данных методов. В рамках данной лекции рассмотрим подходы к созданию цепочек из функциональных инструментов
В рамках цепочек каждый шаг прост и понятен, но вместе они могут решать сложные задачи по обработке данных
Запрос: наибольшие покупки лучших клиентов #
Команда взаимодействия предполагает, что лояльные клиенты совершают также и наибольшие покупки. Надо узнать стоимость наибольшей покупки от лояльных клиентов
Отдельные задачи мы можем успешно решать:
- Получать стоимость наибольшей покупки клиента
- Находить лояльных клиентов, количество покупок больше трёх
Данную задачу можно решить путём формирования цепочки из функций
Последовательность действий для решения задачи #
- Выполнить фильтрацию клиентов для нахождения лояльных
- Для каждого оставшегося клиента найти стоимость наибольшей покупки
- Преобразовать список покупок в их стоимость
- Найти максимальную стоимость
Реализация решения #
Начнём с определения функции:
function biggestPurchasesBestCustomers(customers) {}
Данная функция должна возвращать список из стоимости наибольших покупок
Выполним поиск лояльных покупателей с помощью фильтрации
function biggestPurchasesBestCustomers(customers) {
var bestCustomers = filter(customers, function(customer) {
return customer.purchases.length >= 3;
});
Реализация решения #
Найдём наибольшую стоимость покупки, совершённой данным покупателем. Деталей мы пока что не знаем, но для реализации можно точно использовать map()
function biggestPurchasesBestCustomers(customers) {
var bestCustomers = filter(customers, function(customer) {
return customer.purchases.length >= 3;
});
var biggestPurchases = map(bestCustomers, function(customer) {
return ...; // Надо будет реализовать
});
return biggestPurchases;
};
Реализация решения #
Для поиска наибольшей покупки мы можем воспользоваться reduce()
function biggestPurchasesBestCustomers(customers) {
var bestCustomers = filter(customers, function(customer) {
return customer.purchases.length >= 3;
});
var biggestPurchases = map(bestCustomers, function(customer) {
return reduce(customer.purchases, {total: 0}, // Для каждого клиента
function(biggest, purchase) {
if(biggest.total > purchase.total) // Находим дорогую покупку
return biggest;
else
return purchase;
});
});
return biggestPurchases;
};
Функция решает поставленную задачу, но использует очень много слов для решения
Подходы к упрощению #
Сравним reduce-шаг с функцией max из предыдущей главы
reduce(customer.purchases,
{total: 0},
function(biggest, purchase) {
if(biggest.total > purchase.total)
return biggest;
else
return purchase;
});
reduce(numbers,
Number.MIN_VALUE,
function(m, n) {
if(m > n)
return m;
else
return n;
});
Основное отличие между данными функциями — сравнение свойства total
слева и обычных чисел справа. Обобщим данный подход
Создание общего метода #
// Оригинал
reduce(customer.purchases,
{total: 0},
function(biggest, purchase) {
if(biggest.total > purchase.total)
return biggest;
else
return purchase;
});
// Обобщённая функция
function maxKey(array, init, f) {
return reduce(array, init,
function(biggest, element) {
if(f(biggest) > f(element)) {
return biggest;
} else {
return element;
}
});
}
maxKey(customer.purchases, {total: 0},
function(purchase) { return purchases.total; }
);
Используем эту функцию для упрощения оригинальной функции
Применение метода #
function biggestPurchasesBestCustomers(customers) {
var bestCustomers = filter(customers, function(customer) {
return customer.purchases.length >= 3;
});
var biggestPurchases = map(bestCustomers, function(customer) {
return maxKey(customer.purchases, {total: 0}, function(purchase) {
return purchase.total;
});
});
return biggestPurchases;
}
- Кода в функции стало меньше, она стала понятнее
- Был создан обобщённый инструмент функция
maxKey
, который позволяет получить наибольший элемент массива в соответствии с правилом-функцией - Код стал меньше, но он всё ещё достаточно сложный, его можно упростить
Практика: выбор функции #
Функции maxKey()
и max()
очень похожи. То есть в теории могут иметь очень похожий код
Предположите, что вам необходимо написать одну функцию в терминах другой функции
- Какую функцию следует использовать и почему?
- Напишите код обеих функций
- Сформируйте граф вызовов для этих функций
- Какая из этих двух функций более общая?
-
max()
должна быть реализована в терминахmaxKey()
-
Можно реализовать
max()
с помощьюmaxKey()
, передавая ей функцию возвращающую свой аргументfunction max(array, init) { return maxKey(array, init, function(x) { return x; }); }
-
Граф вызовов:
max() -> maxKey() -> reduce() -> forEach() -> for loop
-
maxKey()
находится нижеmax()
.max()
является более специализированной версиейmaxKey()
Улучшение цепочек № 1: наименование шагов #
Один из способов по облегчению восприятия шагов в цепочке вызовов — добавление имён к шагам
// Оригинал
function biggestPurchasesBestCustomers(customers) {
var bestCustomers = filter(customers, function(customer) { // Шаг №1
return customer.purchases.length >= 3;
});
var biggestPurchases = map(bestCustomers, function(customer) { // Шаг №2
return maxKey(customer.purchases, {total: 0}, function(purchase) {
return purchase.total;
});
});
return biggestPurchases;
}
// Выделенные методы
function selectBestCustomers(customers) {
return filter(customers, function(customer) {
return customer.parchaces.length >= 3;
});
}
function getBiggestPurchases(customers) {
return map(customers, getBiggestPurchace);
}
function getBiggestPurchase(customer) {
return maxKey(customer.purchases, {total: 0}, function(purchase) {
return purchase.total;
});
}
function biggestPurchasesBestCustomers(customers) {
var bestCustomers = selectBestCustomers(customers);
var biggestPurchases = getBiggestPurchases(bestCustomers);
return biggestPurchases;
}
- Целевая функция стала гораздо легче для восприятия
- Выделенные функции тоже понятные сами по себе
Минусы реализации
- Однако сами шаги устроены сложно: функции определяют анонимные встроенные функции
- В рамках шага есть несколько вложенных
return
-выражений - Возможность повторного использования данных функций находится под вопросом
Можно ли добиться повторного использования написанного кода?
Улучшение цепочек № 2: наименование функций обратного вызова #
Второй подход заключается в предоставлении названий для функций обратного вызова, которые изначально объявлены анонимно
// Оригинал
function biggestPurchasesBestCustomers(customers) {
var bestCustomers = filter(customers, function(customer) { // Шаг №1
return customer.purchases.length >= 3;
});
var biggestPurchases = map(bestCustomers, function(customer) { // Шаг №2
return maxKey(customer.purchases, {total: 0}, function(purchase) {
return purchase.total;
});
});
return biggestPurchases;
}
// Выделяем названия для функций обратного вызова
function isGoodCustomer(customer) {
return customer.purchases.length >= 3;
}
function getBiggestPurchase(customer) {
return maxKey(customer.purchases, {total: 0}, getPurchaseTotal);
}
function getPurchaseTotal(purchase) {
return purchase.total;
}
function biggestPurchasesBestCustomers(customers) {
var bestCustomers = filter(customers, isGoodCustomer);
var biggestPurchases = map(bestCustomers, getBiggestPurchase);
return biggestPurchases;
}
- Новые функции легче переиспользовать, так как они решают простые задачи
- Данные функции находятся достаточно низко в графе вызовов, большая часть не зависит от других функций
isGoodCustomer
предоставляет информацию об одном клиенте, можно применять в любой ситуации при работе с одним клиентомselectBestCustomers
работает только лишь с массивом клиентов
Сравнение двух подходов #
function biggestPurchasesBestCustomers(customers) {
var bestCustomers = selectBestCustomers(customers);
var biggestPurchases = getBiggestPurchases(bestCustomers);
return biggestPurchases;
}
function selectBestCustomers(customers) {
return filter(customers, function(customer) {
return customer.parchaces.length >= 3;
});
}
function getBiggestPurchases(customers) {
return map(customers, getBiggestPurchace);
}
function getBiggestPurchase(customer) {
return maxKey(customer.purchases, {total: 0}, function(purchase) {
return purchase.total;
});
}
function biggestPurchasesBestCustomers(customers) {
var bestCustomers = filter(customers, isGoodCustomer);
var biggestPurchases = map(bestCustomers, getBiggestPurchase);
return biggestPurchases;
}
function isGoodCustomer(customer) {
return customer.purchases.length >= 3;
}
function getBiggestPurchase(customer) {
return maxKey(customer.purchases, {total: 0}, getPurchaseTotal);
}
function getPurchaseTotal(purchase) {
return purchase.total;
}
- Обычно второй подход приводит к более понятному исходному коду
- Методы второго подхода легче повторно использовать, т.к. они находятся в нижних частях графа вызова
- Во втором подходе отсутствуют вложенные обратные вызовы
- В конкретном случае (язык, задача, требования) решение о стиле могут отличаться
Пример: почтовые адреса клиентов только с одной покупкой #
Нам необходимо отправить письма-приветствия покупателям, которые совершили только одну покупку в магазине
- Входные данные: список покупателей
- Желаемый результат: почтовые адреса клиентов ровно с одной покупкой
- План:
- Найти клиентов только с одной покупкой
- Преобразовать список клиентов в список почтовых адресов
var firstTimers = filter(customers, function(customer) {
return customer.purchases.length === 1;
});
var firstTimerEmails = map(firstTimers, function(customer) {
return customer.email;
});
// Можно дать имена анонимным встроенным функциям
var firstTimers = filter(customers, isFirstTimer);
var firstTimerEmails = map(firstTimers, getCustomerEmail);
function isFirstTimer(customer) {
return customer.purchases.length === 1;
}
function getCustomerEmail(customer) {
return customer.email;
}
Практика: поиск покупателей #
Отдел маркетинга хочет найти клиентов, которые сделали хотя бы одну покупку на 100$ и как минимум две покупки всего
Реализованная функция должна быть удобна для восприятия
function bigSpenders(customers) {
var withBigPurchases = filter(customers, hasBigPurchase);
var withTwoOrMorePurchases = filter(customers, hasTwoOrMorePurchases);
return withTwoOrMorePurchases;
}
function hasBigPurchase(customer) {
return filter(customer.purchases, isBigPurchase).length > 0;
}
function isBigPurchase(purchase) {
return purchase.total > 100;
}
function hasTwoOrMorePurchases(customer) {
return customer.purchases.length >= 2;
}
Практика: вычисление среднего значения #
Необходимо реализовать функцию, которая вычисляет среднее значение в массиве из чисел
Вариант решения
function average(numbers) {
return reduce(numbers, 0, plus) / numbers.length;
}
function plus(a, b) {
return a + b;
}
Практика: средняя стоимость покупок #
Необходимо вычислить среднюю стоимость покупок для каждого клиента. Можно считать, что функция avegage()
из предыдущего упражнения существует
Вариант решения
function averagePurchaseTotals(customers) {
return map(customers, function(customer) {
var purchasesTotal = map(customer.purnchases, function(purchase) {
return purchase.total;
});
return average(purchaseTotals);
});
}
Оптимизация вызовов функциональных инструментов #
Функции filter()
и map()
создают новые массивы и потенциально добавляют множество элементов в них
- Временные массивы эффективно очищаются сборщиками мусора
- При разумном использовании можно избежать создания множества промежуточных объектов
Вызов нескольких map()
#
Если для решения задачи необходимо вызвать несколько раз map, то вместо этого можно вызвать один раз с применением комбинированного метода
var names = map(customers, getFullName);
var nameLengths = map(names, stringLength);
var nameLengths = map(customers, function(customer) {
return stringLength(getFullName(customer));
});
Вызов нескольких фильтров #
При выполнении нескольких фильтров подряд мы де-факто объединяем условия с помощью логического И
var goodCustomers = filter(customers, isGoodCustomer);
var withAddress = filter(goodCustomers, hasAddress);
var withAddress = filter(customers, function(customer) {
return isGoodCustomer(customer) && hasAddress(customer);
});
Вызов map()
с последующим reduce()
#
Мы обсуждали, что с помощью reduce() можно реализовать другие функции. Даже можно реализовать совмещение функций
var purchaseTotals = map(purchases, getPurchaseTotal);
var purchaseSum = recude(purchaseTotals, 0, plus);
var purchaseSum = reduce(purchases, 0, function(total, purchase) {
return tolal + getPurchaseTotal(purchase);
});
Преобразование существующих циклов for #
Можно выделить 2 стратегии по преобразованию
- Понять назначение данного цикла и переписать заново с использованием функциональных инструментов
- Преобразовать на основании подсказок
Первый подход в целом работает, но до какого-то предела в сложности цикла
var answer = [];
var window = 5;
for(var i = 0; i < array.length; i++) {
var sum = 0;
var count = 0;
for(var w = 0; w < window; w++) {
var idx = i + w;
if(idx < array.length) {
sum += array[idx];
count += 1;
}
}
answer.push(sum/count);
}
// Выделим элементы в коде
var answer = []; // Результатом вычислений является массив
var window = 5; // Магическая переменная
// Обход элементов массива array
for(var i = 0; i < array.length; i++) {
var sum = 0;
var count = 0;
// Цикл по промежутку от 0 до 4
for(var w = 0; w < window; w++) {
var idx = i + w; // Новый индекс
if(idx < array.length) {
sum += array[idx];
count += 1; // Вычисление значений
}
}
answer.push(sum/count); // Добавление их в массив
}
Подсказка № 1: создавайте данные для обхода #
При использовании низкоуровневых инструментов мы не формируем промежуточные данные, которые получаются при использовании инструментов map()
, filter()
, …
Можно попытаться выделить элементы, по которым происходит итерирование
// Оригинал
var answer = [];
var window = 5;
for(var i = 0; i < array.length; i++) {
var sum = 0;
var count = 0;
// Проход по части массива array
for(var w = 0; w < window; w++) {
var idx = i + w;
if(idx < array.length) {
sum += array[idx];
count += 1;
}
}
answer.push(sum/count);
}
var answer = [];
var window = 5;
for(var i = 0; i < array.length; i++) {
var sum = 0;
var count = 0;
var subarray = array.slice(i, i + window);
for(war w = 0; w < subarray.length; w++) {
sum += subarray[w];
count += 1;
}
answer.push(sum/count);
}
Подсказка № 2: обрабатывайте массив целиком #
После выделения конкретных объектов, по которым происходит итерирование, можно применить функциональные инструменты для выполнения действий
В рамках примера для выделенного массива вычисляется среднее значение, можно воспользоваться функцией average
var answer = [];
var window = 5;
for(var i = 0; i < array.length; i++) {
var sum = 0;
var count = 0;
var subarray = array.slice(i, i + window);
for(war w = 0; w < subarray.length; w++) {
sum += subarray[w];
count += 1;
}
answer.push(sum/count);
}
// Используем функцию average
var answer = [];
var window = 5;
for(var i = 0; i < array.length; i++) {
var subarray = array.slice(i, i + window);
answer.push(average(subarray));
}
Подсказка № 3: делайте много маленьких шагов #
Получившийся for-цикл проходит не по элементам массива, а по подмножествам массива. В результате мы не можем применить известные функции высшего порядка напрямую
Подмножества получаются по индексам элементов, а не по значению элементов
- Мы можем создать массив из индексов и проходит уже по нему
- Использовать функциональные инструменты на массиве с индексами
var indicies = [];
for(var i = 0; i < array.length; i++)
indicies.push(i);
var window = 5;
var answer = map(indicies, function(index) {
var subarray = array.slice(index, index + window);
return average(subarray);
});
Подсказка № 3: делайте много маленьких шагов #
Мы можем улучшить подход, упростив функцию, которую мы передаём в map()
:
- Отдельно создать массив из выборок
- Отдельно вычислить среднее значение для каждой выборки
var indicies = [];
for(var i = 0; i < array.length; i++)
indicies.push(i);
var window = 5;
var windows = map(indicies, function(index) {
return array.slice(i, i + window);
});
var answer = map(windows, average);
Подсказка № 3: делайте много маленьких шагов #
Мы также можем провести рефакторинг, выделив функцию range()
, которая будет создавать массив из индексов нужного промежутка
function range(start, end) {
var result = [];
for(var i = start; i < end; i++)
result.push(i);
return result;
}
var indicies = range(0, array.length);
var window = 5;
var windows = map(indicies, function(index) {
return array.slice(i, i + window);
});
var answer = map(windows, average);
Сравнение подходов #
Императивный подход
var answer = [];
var window = 5;
for(var i = 0; i < array.length; i++) {
var sum = 0;
var count = 0;
for(var w = 0; w < window; w++) {
var idx = i + w;
if(idx < array.length) {
sum += array[idx];
count += 1;
}
}
answer.push(sum/count);
}
Функциональный подход
var indicies = range(0, array.length);
var window = 5;
var windows = map(indicies, function(index) {
return array.slice(i, i + window);
});
var answer = map(windows, average);
// Инструмент общего назначения
function range(start, end) {
var result = [];
for(var i = start; i < end; i++)
result.push(i);
return result;
}
- Код в функциональном подходе получился короче, мы смогли получить инструмент общего назначения
- Задача кода стала понятнее:
- На вход получаем массив из чисел
- Для каждого числа формируем окно
- Вычисляем среднее значение для каждого окна
Вопросы: в каком месте графа вызовов стоит расположить новый метод range()
? Что его позиция говорит о возможности повторного применения, тестирования и поддержке?
Сводка подсказок по преобразованию кода #
Создавайте данные для обхода #
Функциональные инструменты ориентированы на использование с массивами данных. Необходимо явно выделить данные массиве в коде
Выполняйте обработку массива целиком #
Необходимо определить функцию высшего порядка, которая наиболее точно подходит для решения задачи путём обработки всего массива
Делайте много маленьких шагов #
Если кажется, что код стал выполнять много действий, то выделение небольших и понятных последовательностей действий может помочь в выполнении преобразования
Заменяйте условия вызовом filter()
#
Если в рамках цикла for присутствует условие, то оно зачастую направлено на пропуск элементов. Такие условия можно удобно заменить на вызов filter()
Выделяйте функции-помощники #
Помимо map()
, filter()
и reduce()
существует множество функциональных инструментов. Рекомендуется выделять функции-помощники, чтобы познакомиться с другими инструментами. Давайте им имена и повторно применяйте их
Экспериментируйте, чтобы совершенствоваться #
Для того, чтобы научиться эффективному использованию функциональных инструментов необходимо их применять, практиковаться. При этом не стоит ограничивать себя только известными схемами комбинирования данных инструментов
Практика #
Рассмотрите следующий код из приложения магазина. Преобразуйте его в цепочку вызова функциональных инструментов. Существует множество способов решения этой задачи
function shoesAndSocksInventory(products) {
var inventory = 0;
for(var p = 0; p < products.length; p++) {
var product = products[p];
if(product.type === "shoes" || product.type === "socks") {
inventory += product.numberInInventory;
}
}
return inventory;
}
Вариант решения #
function shoesAndSocksInventory(products) {
var shouesAndSocks = filter(products, function(product) {
return product.type === "shoes" || product.type === "socks";
});
var inventories = map(shoesAndSocks, function(product) {
return product.numberInInventory;
});
return reduce(inventories, 0, plus);
}
Подсказки для отладки цепочки функциональных вызовов #
Во время работы с функциональным подходом код может стать слишком абстрактным и сложным для понимания особенно, когда что-то идёт не так
Давайте хорошие имена шагам #
Достаточно легко забыть как выглядят данные после прохождения ряда этапов по обработке. Рекомендуется давать понятные названия переменным, хранящим промежуточные этапы обработки
Добавляйте отладочный вывод #
Для того чтобы понять состояние данных после очередного этапа обработки, добавляйте отладочный вывод между этапами. После выполняйте свой код и анализируйте отладочный вывод
Следуйте за своими типами #
Каждый функциональный инструмент работает с конкретными типами данных. В языке JavaScript они присутствуют, хотя и не проверяются во время компиляции
Используйте информацию о типах данных для достижения результата
- Функция
map()
возвращает новый массив. Что находится внутри? То, что возвращает переданная функция. - Функция
filter()
возвращает массив из тех же данных, что мы передаём в качестве аргумента - Функция
reduce()
возвращает тип данных, которые возвращает переданная ему функция
Используя эту информацию можно достаточно быстро разобраться в назначении кода
Другие функциональные инструменты #
Рассмотрим другие функции, которые могут быть полезны для решения задач. Это только небольшой набор методов из большого арсенала
pluck()
— получение значения полей
#
Данная функция позволяет создать массив из значений полей объектов, которые находятся в оригинальном массиве
function pluck(array, field) {
return map(array, function(object) {
return object[field];
});
}
var prices = pluck(products, 'price');
// Вариант
function invokeMap(array, method) {
return map(array, function(object) {
return object[method]();
});
}
concat()
— убрать уровень вложенности массива
#
Данная функция убирает один уровень вложенности массива. То есть делает из двухмерного массива одномерный со всеми данными из оригинального массива
function concat(arrays) {
var result = [];
forEach(arrays, function(array) {
forEach(array, function(element) {
result.push(element);
});
});
return result;
});
var purchaseArrays = pluck(customers, "purchases");
var allPurchases = concat(purchaseArrays);
// Вариант
function concatMap(array, f) {
return concat(map(array, f));
}
frequenciesBy
и groupBy
— подсчёт частоты и группировка элементов
#
function frequenciesBy(array, f) {
var result = {};
forEach(array, function(element) {
var key = f(element);
if(result[key]) result[key] += 1;
else result[key] = 1;
});
return result;
}
var howMany = frequenciesBy(products, function(p) {
return p.type;
});
function groupBy(array, f) {
var result = {};
forEach(array, function(element) {
var key = f(element);
if(result[key]) result[key].push(element);
else result[key] = [element];
});
return ret;
});
var groups = groupBy(range(0, 10), isEven);
Где найти функциональные инструменты #
JavaScript: библиотека Lodash #
Библиотека Lodash содержит большое количество функций, «недостающие элементы стандартной библиотеки»
Clojure: стандартная библиотека #
Стандартная библиотека содержит большое количество функций для работы с последовательностями. Ключевая проблема — их очень много
Python: ряд библиотека #
- Библиотека toolz — набор библиотек для итерирования, работы с функциями высшего порядка и словарями
- Библиотека pyrsistent — набор классов, предоставляющих неизменяемые данные
- Язык Coconut — надстройка над языком Python, предоставляющий более широкие возможности
- Встроенные модули функционального программирования
Haskel Prelude #
Данный стандартный модуль языка Haskell содержит небольшой набор функциональных инструментов, которые предоставляют мощные возможности для функционального программирования. Документация, краткий обзор
Java #
Начиная с 8 выпуска языка в язык Java стали добавлять расширенную поддержку функциональных инструментов
- Лямбда-выражения. В коде можно писать лямбда-выражения, которые выглядят как анонимные встроенные функции, способные захватить контекст определения
- Функциональные интерфейсы. Данные интерфейсы позволяют просто описывать объекты-действия в качестве аргументов функции
- Function — функция одного аргумента, удобна для
map()
- Predicate — функция одного аргумента, возвращающая
true
илиfalse
, удобна дляfilter()
- BiFunction — функция двух аргументов, возвращающая одно значение. Удобна для
reduce()
- Function — функция одного аргумента, удобна для
- Stream API — подход к организации конвейеров по обработке данных с помощью функциональных инструментов
Java: ряд библиотек и языков #
- Библиотека vavr.io — набор неизменяемых коллекций и методов для работы с ними
- Библиотека Functional Java — набор неизменяемых коллекций и методов для работы с ними
- Язык Kotlin — язык для платформы JVM, включающий большое количество функциональных инструментов из стандартной библиотеки
- Язык Scala — язык для платформы JVM, популяризовавший функциональное программирование для данной платформы
Ruby #
Язык Ruby предлагает большое количество функциональных инструментов:
- Блоки в качестве анонимных функций, захватывающих контекст определения
- Модуль Enumerable предлагает множество методов для итерирования по стандартным структурам данных без их модификации
- Для создания объектов-ссылок на методы объектов можно использовать метод Object#method()
Стандартная библиотека JavaScript #
В рамках курса не рассматривается вопрос о функциональном программировании на языке JavaScript, не рассматриваются встроенные функциональные инструменты
Рассмотрим кратко встроенные функциональные средства языка JavaScript
// Подход из книги
var customerNames = map(customers, function(c) {
return c.firstName + " " + c.lastName;
});
// Встроенные инструменты
var customerNames = customers.map(function(c) {
return c.firstName + " " + c.lastName;
});
Ввиду того, что методы map
встроены в массив и создают новый массив, то можно выполнять вызов данных методов последовательно
// Подход из книги
var window = 5;
var indicies = range(0, array.length);
var windows = map(indicies, function(i) {
return array.slice(i, i + window);
});
var answer = map(windows, average);
// Цепочка методов
var window = 5;
var answer =
range(0, array.length)
.map(function(i) {
return array.slice(i, i + window);
})
.map(average);
В актуальной версии JavaScript добавили возможность удобного описания коротких анонимных функций, которые упрощают использование методов map
, filter
и reduce
var window = 5;
var answer =
range(0, array.length)
.map(i => array.slice(i, i + window))
.map(average);
Также функция map
помимо самого элемента передаёт ещё и индекс элемента, что может заменить собой другие функции
var window = 5;
var average = array => array.reduce((sum, e) => sum + e, 0) / array.length;
var answer = array.may((e, i) => array.slice(i, i + window)).map(average);
reduce()
для вычисления значений
#
До этого мы рассматривали reduce()
только как средство для вычисления значения путём комбинирования различных элементов
Другой способ использования метода — создание значений
Рассмотрим сценарий. Мы записали названия товаров, которые были добавлены в корзину пользователем:
var itemsAdded = ["shirt", "shoes", "shirt", ...]
Можем ли мы сформировать текущее состояние корзины, имея эту информацию?
Ответ: да, с использованием reduce()
Технический шаг: сформируем код по созданию shoppingCart из списка товаров
var shoppingCart = reduce(itemsAdded, {}, function(cart, item) {
});
Предположим, что корзина ещё не содержит товар, выбранный пользователем
var shoppingCart = reduce(itemsAdded, {}, function(cart, item) {
if(!cart[item])
return add_item(cart,
{name: item, quantity: 1, price: priceLookup(item)});
});
Реализуем второй вариант: когда товар уже есть в корзине
var shoppingCart = reduce(itemsAdded, {}, function(cart, item) {
if(!cart[item])
return add_item(cart,
{name: item, quantity: 1, price: priceLookup(item)});
else {
var quantity = cart[item].quantity;
return setFieldByName(cart, item, 'quantity', quantity + 1);
}
});
Анализ результатов #
Функция, которая передаётся как аргумент reduce()
, является полезной сама по себе и её можно добавить в барьер из абстракций для корзины
function addOne(cart, item) {
if(!cart[item])
return add_item(cart,
{name: item, quantity: 1, price: priceLookup(item)});
else {
var quantity = cart[item].quantity;
return setFieldByName(cart, item, 'quantity', quantity + 1);
}
}
- Мы можем создать корзину в любой момент на основании названий товаров. Не надо поддерживать корзину!
- На основании истории добавления товаров легко реализовать функции отмены и повтора действий. Этот подход называется event sourcing
Реализация отмены действия #
На предыдущем этапе мы реализовали создание корзины из списка товаров. Однако этого недостаточно, чтобы реализовать отмену действия, т.к. оно может быть как добавлением, так и удалением
Необходимо расширить массив информацией о действиях пользователя
var itemOps = [["add", "shirt"], ["add", "shoes"], ["remove", "shirt"],
["add", "scoks"], ["remove", "hat"], ...];
Теперь можно легко реализовать поддержку двух операций:
var shoppingCart = reduce(itemOps, {}, function(cart, itemOp) {
var op = itemOp[0];
var item = itemOp[1];
if(op === "add") return addOne(cart, item);
if(op === "remove") return removeOne(cart, item);
});
Рассмотрим реализацию метода removeOne
function removeOne(cart, item) {
if(!cart[item])
return cart;
else {
var quantity = cart[item].quantity;
if(quantity == 1)
return remove_item_by_name(cart, item);
else
return setFieldByName(cart, item, "quantity", quantity - 1);
}
};
Анализ реализации #
- Теперь на основе всех действий пользователя мы можем формировать актуальное состояние корзины
- Была применена важная техника: расширение данных.
- В массиве действий находится название действий и дополнительная информация, расширение.
- Данная техника часто используется программистами на ФЯП
- Позволяет выстраивать более качественные цепочки из инструментов
Практика #
Компания планирует принять участие в соревнованиях по софтболу и ей необходимо сформировать команду для участия в соревнованиях. Для каждого сотрудника была проведена оценка его способностей для той позиции, в которой они лучше всего показали себя. Они уже отсортированы по набранным баллам
var evaluations = [
{name: "John", position: "pitcher", score: 13},
{name: "Jane", position: "catcher", score: 10},
{name: "Harry", position: "pitcher", score: 5},
...
];д
Ваша задача — сформировать ростер компании, который должен выглядеть так:
var roster = {
"pitcher": "John",
"catcher": "Jane",
"first base": "Ellen"
};
Вариант решения
var roster = reduce(evaluations, {}, function(roster, evaluation) {
var position = evaluation["position"];
if(roster[position])
return roster;
return setObject(roster, position, evaluation["name"]);
});
Практика #
Для участия в соревнованиях необходимо провести оценку возможностей сотрудников для каждой из позиции в игре. Для решения этой задачи для одного человека у нас есть метод recommendedPosition(name)
, которому передаётся имя сотрудника и который возвращает подходящую позицию
Вам был предоставлен список имён сотрудников. На его основании необходимо сформировать список рекомендованных позиций в формате
{
name: "Jane",
position: "Catcher"
}
var employeeNames = ["John", "Jane", "Harry", ...];
Вариант решения
var positions = map(employeeNames, function(name) {
return {name: name, position: recommendedPosition(name)};
});
Практика #
После определения наилучшей позиции нам необходимо оценить навык конкретно для данной позиции, чтобы найти наиболее подходящего сотрудника для каждой позиции. Чем выше полученный бал, тем больше этот сотрудник подходит для данной позиции
Для оценки пригодности сотрудника есть метод scorePlayer(name, position)
, который возвращает балл
Вам предоставили список из рекомендованных позиций (из предыдущей практики). На его основе необходимо подготовить расширенный список, включающий информацию как о позиции, так и баллы
{ name: "Jane", position: "catcher", 10 }
var recommendations = [{name: "Jane", position: "catcher"},
{name: "John", position: "pitcher"}, ...];
Вариант решения
var evaluations = map(recommendations, function(recommendation) {
return objectSet(recommendation, "score",
scorePlayer(recommendation.name, recommendation.position));
});
Практика #
Теперь необходимо объединить все три предыдущие практики в рамках одно функционирующей системы. На вход ей подаётся список сотрудников, из которых нужно сформировать хорошо функционирующую команду
Помимо разработанных методов также потребуются методы
sortBy(array, f)
— возвращает копию массива, отсортированного по признаку формируемому переданной функциейreverse(array)
— возвращает копию массива, в которой элементы находятся в обратном порядке
var employeeNames = ["John", "Harry", "Jane", ...];
Вариант решения
var employeeNames = ["John", "Harry", "Jane", ...];
var recommendations = map(employeeNames, function(name) {
return {name: name, position: recommendedPosition(name)};
});
var evaluations = map(recommendations, function(recommendation) {
return objectSet(position, "score": scorePlayer(recommendation.name, recommendation.position));
});
var evaluationsAscending = sortBy(evaluations, function(evaluation) {
return evaluation.score;
});
var evaluationsDescending = reverse(evaluationsAscending);
var roster = reduce(evaluationsDescending, {}, function(roster, evaluation) {
var position = evaluation.position;
if(roster[position])
return roster;
return objectSet(roster, position, evaluation.name);
});
Выравнивание инструментов #
Зачастую обработку информации с помощью функциональных инструментов оформляют в виде цепочки вызовов методов, которые находятся один под другим. Длинная цепочка не только радует глаз программиста, но также показывает, что функциональные инструменты используются верно
Рассмотрим решение задачи вычисления среднего значения на разных языках
ES6 JavaScript #
function movingAverage(numbers) {
return numbers
.map((_e, i) => numbers.slice(i, i + window))
.map(average);
}
JavaScript + Lodash #
function movingAverage(numbers) {
return _.chain(numbers)
.map(function(_e, i) { return numbers.slice(i, i + window) }
.map(average)
.value();
}
Java8 с потоками #
public static double average(List<Double> numbers) {
return numbers.
.stream()
.reduce(0.0, Double::sum) / numbers.size();
}
public static List<Double> movingAverage(List<Double> numbers) {
return IntStream
.range(0, numbers.size())
.mapToObj(i -> numbers.subList(i, Math.min(i + 3, numbers.size())))
.map(Utils::average)
.collect(Collectors.toList());
}
C# #
public static IEnumerable<Double> movingAverage(IEnumerable<Double> numbers) {
return Enumerable
.Range(0, numbers.Count())
.Select(i => numbers.ToList().GetRange(i, Math.Min(3, numbers.Count() - i)))
.Select(l => l.Average());
}
Заключение #
В данной лекции мы рассмотрели процесс построения цепочек из функций для обработки данных. На каждом этапе происходит преобразование данных в форму, которая чуть ближе к желаемому финальному результату
- Можно комбинировать функциональные инструменты в многоступенчатые цепочки. Цепочки строятся из небольших понятных шагов
- Один из взглядов на построение цепочки — описание в формате декларативного языка для запросов наподобие SQL
- Часто необходимо создать новые данные или изменить существующие данные, чтобы сделать следующие шаги по обработке проще. Ищите способы для представления неявной информации в формате явных данных
- Существует множество функциональных инструментов. Их можно создавать в результате рефакторинга исходного кода. Их можно отыскать в библиотеках и других языках программирования
- Функциональные инструменты проникают в другие языки, которые традиционно не поддерживают функциональный подход, например Java. Используйте их, если это упрощает код