Васильев Андрей Михайлович, 2022
Версии презентации
map
, filter
и reduce
Задача прохождения по массиву встречается очень часто. В рамках этого занятия рассмотрим базовые функциональные инструменты, позволяющие решать эту задачу лего
В связи с увеличенным количеством покупок и присутствия на рынке компания собирается создать новый отдел: отдел взаимодействия с клиентами
Основная задача этого отдела — активное общение с пользовательской базой, в частности путём отправки почтовых сообщений: о новых акциях магазина, отправка официальных писем и т.д.
Основная проблема — надо посылать письма именно заинтересованным пользователям, а не всем сразу и тем более не неправильным адресатам
Новая команда собирается из специалистов отдела разработки, маркетинга и службы поддержки
function emailsForCustomers(customers, goods, bests) {
var emails = [];
for(var i = 0; i < customers.length; i++) {
var customer = customers[i];
var email = emailForCustomer(customer, goods, bests);
emals.push(email);
}
return emails;
}
forEach
function emailsForCustomers(customers, goods, bests) {
var emals = [];
forEach(customers, function(customer) {
var email = emailForCustomer(customer, goods, bests);
emails.push(email);
});
return emails;
}
Ключевая задача функции - создать новый массив на основании данных, которые присутствуют в оригинальном массиве
Для решения этой задачи в ФЯП используется функция map()
. Она принимает 2 аргумента: массив и функцию, которая формирует элементы нового массива
map()
из примеров
#
function emailsForCustomers(customers, goods, bests) {
var emals = [];
forEach(customers, function(customer) {
var email = emailForCustomer(customer, goods, bests);
emails.push(email);
});
return emails;
}
function customerFullNames(customers) {
var fullNames = [];
forEach(customers, function(customer) {
var name = customer.name + ' ' + customer.lastName;
fullNames.push(name);
});
return fullNames;
}
function biggestPurchasePerCustomer(customers) {
var purchases = [];
forEach(customers, function(customer)) {
var purchase = biggestPurchase(customer);
purchases.push(purchase);
});
return purchases;
}
function customerCities(customers) {
var cities = [];
forEach(customers, function(customer) {
var city = customer.address.city;
cities.push(city);
});
return cities;
}
Дублирование в этих функциях можно исправить с помощью рефакторинга замены тела функции с помощью функции обратного вызова
// Оригинал
function emailsForCustomers(customers, goods, bests) {
var emals = [];
forEach(customers, function(customer) {
var email = emailForCustomer(customer, goods, bests);
emails.push(email);
});
return emails;
}
// Создаём функцию, содержащую общие элементы
function map(array, f) {
var newArray = [];
forEach(array, function(element) {
newArray.push(f(element)); // Вызов
});
return newArray;
}
// Используем данную функцию
function emailsForCustomers(custmers, goods, bests) {
return map(customers, function(customer) {
return emailForCustomer(customer, goods, bests);
});
}
Мы смогли создать одну из мощных базовых функций функциональных языков программирования, которая реализует общий подход к итерированию
map()
#
function map(array, f) { // Принимает массив и функцию
var newArray = []; // Создаёт новый массив
forEach(array, function(element) {
newArray.push(f(element)); // Создаёт новый элемент
// и записывает в массив
});
return newArray; // Возвращает созданный массив
}
Функция map()
создаёт новый массив на основании существующего массива и правила генерации. Обе части функции map()
передаёт пользователь
Наиболее просто применять map()
на функциях-вычислениях, так как переданная функция будет вызываться функцией map()
map()
#
function emailForCustomers(customers, goods, bests) {
// Передаём массив и функцию-генератор новых значений
return map(cestomers, function(customer) {
return emailForCustomer(customer, goods, bests);
});
}
Почему мы называли переменную customer
?
map()
будет вызывать переданную функцию для каждого элемента массива. Массив состоит из описаний покупателей, т.е. в качестве аргумента данная функция будет получать ссылку на описание одного конкретного покупателя
Большинство функций определяется в рамках глобального контекста. Данную функцию можно вызвать по имени в любой части приложения по её имени
function greet(name) {
return "Hello, " + name;
}
var friendsGreetings = map(friendsNames, greet);
Функция может быть определена в том месте, где она будет использована. Пример такого подхода: определение функции в момент вызова другой функции
var friendsGreetings = map(friendsNames, function(name) {
return "Hello, " + name;
});
Функцию можно определить внутри функции. Обратиться к ней можно будет в тех местах, где есть доступ к соответствующей переменной
Такой подход полезен, если функции необходимо иметь доступ к другим переменным внутри данного метода
function greetEverybody(friends) {
var greeting;
if(language === "English")
greeting = "Hello, ";
else
greeting = "Salut, ";
var greet = function(name) {
return greeting + name;
};
return map(friends, greet);
}
map()
#
Предположим, что нам необходимо сформировать список email-адресов всех клиентов
map(customers, function(customer) {
return customer.email;
});
Нам де-факто потребовалось только реализовать функцию, которая получает данные одного покупателя и формирует на её основе email-адрес
Потенциальные проблемы данного определения:
customer
может быть не определён, может не иметь свойства email
null
Нам необходимо сформировать поздравительные открытки для наших покупателей. Для этого нам необходимо знать имя, фамилию и адрес каждого клиента.
Входные данные:
customers
— массив из объектов, описывающих покупателейcustomer.firstName
, customer.lastName
, customer.address
содержат необходимую информациюВариант решения
map(customers, function(customer) {
return {
firstName: customer.firstName,
lastName: customer.lastName,
address: customer.address
}
});
Нам нужен список активных клиентов, которые активно пользуются услугами магазина. Под лучшим клиентом мы подразумеваем клиента, совершившего три или более покупок
Для решения задачи нельзя использовать map()
, так как данный метод всегда возвращает новый массив, количество элементов которого совпадает с изначальным массивом
Рассмотрим императивную реализацию:
function selectBestCustomers(customers) {
var newArray = [];
forEach(customers, function(customer) {
if(customer.purchases.length >= 3)
newArray.push(customer);
});
return newArray;
}
Функция очень похожа на map()
однако она не создаёт новый элемент, а создаёт новый массив, в который входят только искомые элементы
filter()
на основании примеров
#
function selectBestCustomers(customers) {
var newArray = [];
forEach(customers, function(customer) {
if(customer.purchases.length >= 3)
newArray.push(customer);
});
return newArray;
}
function selectCustomersBefore(customers, date) {
var newArray = [];
forEach(customers, function(customer) {
if(customer.signupDate < date)
newArray.push(customer);
});
return newArray;
}
function selectCustomersAfter(customers, date) {
var newArray = [];
forEach(customers, function(customer) {
if(customer.signupDate > date)
newArray.push(customer);
});
return newArray;
}
function singlePurchaseCustomers(customers) {
var newArray = [];
forEach(customers, function(customer) {
if(customer.purchases.length === 1)
newArray.push(customer);
});
return newArray;
}
function selectBestCustomers(customers) { // Общий заголовок
var newArray = []; //
forEach(customers, function(customer) { //
if(customer.purchases.length >= 3)
newArray.push(customer); // Общее окочнание
}); //
return newArray; //
}
function selectBestCustomers(customers) {
return filter(customers, function(customer) {
return customer.purchases.length >= 3;
});
}
function filter(array, f) {
var newArray = [];
forEach(array, function(element) {
if(f(element))
newArray.push(element);
});
return newArray;
}
filter()
представляет общий интерфейс для итерирования по элементам массиваfilter()
#
function filter(array, f) { // Принимает массив и функцию
var newArray = []; // Создаёт новый массив
forEach(array, function(element) {
if(f(element)) // Вызвает f для принятия решения
newArray.push(element); // Добавляет элемент в новый массив
});
return newArray; // Возвращает новый массив
}
map()
filter()
делает выборку из оригинального массива согласно правилу, определённому внутри функции-аргументаНеобходимо создать массив из покупателей, которые зарегистрировались в системе, но пока что ещё не совершили ни одной покупки
filter(customers, function(customer) {
return customer.purchases.length === 0;
});
Функция filter()
позволяет нам получить нужную выборку из массива с сохранением последовательности элементов в оригинальном массиве
Мы рассматривали проблему того, что функция map()
может вернуть null
-элементы и это нормальное поведение. Можно использовать null-фильтр для отбрасывания этих элементов
var allEmails = map(customers, function(customer) {
return customer.email; // Данное поле может быть null
});
var emailsWithoutNulls = filter(allEmails, function(email) {
return email !== null; // Выбираем не null-значения
});
Функции map()
и filter()
успешно дополняют друг друга
Команде маркетинга необходимо сделать выборку из всех клиентов, чтобы проверить свою новую стратегию. Им необходимо выбрать примерно треть клиентов
Для решения данной задачи достаточно выбрать клиентов, чей ID делится без остатка на 3
Входные данные:
customers
id
: customer.id
%
Результат:
testGroup
содержит массив клиентов, вошедших в тестовую выборкуnonTestGroup
содержит массив клиентов, не вошедших в тестовую выборкуПример решения
var testGroup = filter(customers, function(customer) {
return customer.id % 3 === 0;
});
var nonTestGroup = filter(customers, function(customer) {
return customer.id % 3 !== 0;
});
Необходимо вычислить число покупок для всех клиентов, то есть общую характеристику для всего массива, а не для определённого элемента
Для решения задачи нельзя использовать map()
или filter()
, которые возвращают массив на основании существующего массива, а нам необходимо число
Рассмотрим императивную реализацию:
function countAllPurchases(customers) {
var total = 0;
forEach(customers, function(customer) {
total = total + customer.purchaces.length;
});
return total;
}
Данная функция проходит по массиву как map()
и filter()
, но внутри происходит агрегированное вычисление значения на основе элементов массива
reduce()
на основании примеров
#
function countAllPurchases(customers) {
var total = 0;
forEach(customers, function(customer) {
total = total + customer.purchaces.length;
});
return total;
}
function customersPerCity(customers) {
var cities = {};
forEach(customers, function(customer) {
cities[customer.address.city] += 1;
});
return cities;
}
function concatenateArrays(arrays) {
var result = [];
forEach(arrays, function(array) {
result = result.concat(array);
});
return result;
}
function biggestPurchase(purchaces) {
var biggest = {total:0};
forEach(purchaces, function(purchase) {
biggest = biggest.total > purchase.total ?
biggest : purchase;
});
return total;
}
function countAllPurchases(customers) { // Общее начало
var total = 0; // Уникальная инициализация
forEach(customers, function(customer) { //
total = total + customer.purchases.length;
}); // Общее окончание
return total; //
}
function countAllPurchases(customers) {
return reduce(customers, 0, function(total, customers) {
return total + customers.purchase.length;
});
}
function reduce(array, init, f) {
var accum = init;
forEach(array, function(element) {
accum = f(accum, element);
});
return accum;
}
reduce()
предоставляет общий интерфейс итерированияreduce()
#
function reduce(array, init, f) { // Массив, изначальное значение, функция
var accum = init; // Инициализация накопителя
forEach(array, function(element) {
accum = f(accum, element); // Вычисление следующего значения
// переменной-накопителя
});
return accum; // Вернуть вычисленное значение
}
reduce()
вычисляет значение путём обхода массиваДопустим, что у нас есть массив из строк и нам необходимо конкатенировать их
reduce(strings, "", function(accum, string) {
return accum + string;
});
reduce()
#
Функция принимает 3 аргумента, разных по своей природе. Функция-аргумент принимает 2 аргумента, что усложняет работу с данной функцией
В разных языках программирования порядок данных аргументов будет отличаться! В рамках курса порядок аргументов у функции выстраиваются следующим образом:
Другая проблема — определение изначального значения
Бухгалтерскому отделу в рамках своей работы необходимо складывать и перемножать числа. Реализуйте методы sum(numbersn)
и product(numbers)
, выполняющие данные операции над массивами из чисел numbers
Вариант решения
function sum(numbers) {
return reduce(numbers, 0, function(total, num) {
return total + num;
});
}
function product(numbers) {
return reduce(numbers, 1, function(total, num) {
return total * num;
});
}
Реализуйте 2 метода, которые позволяют найти минимальный и максимальный элемент в массиве из чисел
Входные данные:
function min(numbers) {
return reduce(numbers, Number.MAX_VALUE, function(m, n) {
if(m < n) return m;
else return n;
});
}
function max(numbers) {
return reduce(numbers, Number.MIN_VALUE, function(m, n) {
if(m > n) return m;
else return n;
});
}
Ответите на следующие вопросы, которые рассматривают работу функций в предельных ситуациях
map()
, если ей передать пустой массив: map([], xToY)
?filter()
, если ей передать пустой массив: filter([], isGood)
?reduce()
, если ей передать пустой массив: reduce([], init, combine)
?map()
, если ей передать функцию, которая возвращает оригинальный элемент: map(array, function(x) { return x; })
?filter()
, если ей передать функцию, которая всегда возвращает true
: filter(array, function(_x) { return true; })
?filter()
, если ей передать функцию, которая всегда возвращает false
: filter(array, function(_x) { return false; })
?[]
[]
init
array
array
[]
reduce()
#
Данная функция является настолько мощной, что с её помощью можно реализовать map()
и filter()
, но не наоборот
Если спроектировать действия пользователей как список действий, то реализация функций «отмена» и «повтор» сводится к изменению списка и выполнению reduce()
При использовании модели из предыдущего пункта легко реализовать просмотр действий пользователя — начинаем с пустого состояния и «проигрываем» действия пользователя с помощью reduce()
В ряде сред программирования во время точки остановки можно изменять предыдущие фреймы. После исправления ошибки в одном из фреймов можно продолжить выполнение приложения. Эта функция реализована с помощью reduce()
map()
и filter()
#
Реализуйте методы map()
и filter()
с помощью reduce()
Вариант решения
function map(array, f) {
return reduce(array, [], function(ret, item) {
return ret.concat([f(item)]); // Не изменяем аргумент
});
}
function map(array, f) {
return reduce(array, [], function(ret, item) {
ret.push(f(item)); // Изменяем аргумент, более эффективная
return ret; // реализация
});
}
function filter(array, f) {
return reduce(array, [], function(ret, item) {
if(f(item)) return ret.concat([item]); // Не изменяем аргумент
else return ret;
});
}
function filter(array, f) {
return reduce(array, [], function(ret, item) {
if(f(item))
ret.push(item); // Изменяем аргумент, более эффективно
return ret;
});
}
Подход с изменением аргумента оказался более эффективным. Это нарушает изначальную рекомендацию по использованию функций-вычислений в качестве аргумента функции reduce()
В данном случае это нормально, т.к. функция-действие определена и используется исключительно внутри методов map()
и filter()
map()
трансформирует массив в новый массив путём применения функции к каждому элементу
#
map(array, function(element) {
...
return newElement; // Надо вернуть новый элемент
});
filter()
выбирает подмножество элементов массива в новый массив
#
filter(array, function(element) {
...
return true; // Надо вернуть true или false
});
reduce()
вычисляет агрегированное значение по элементам массива
#
reduce(array, 0, function(accum, element) {
...
return combine(accum, element); // Вернуть следующее значение накопителя
});
map()
, filter()
и reduce()
являются наиболее часто используемыми инструментами ФЯПmap()
, filter()
и reduce()
являются специализированными «циклами» по массивам. Они заменяют циклы на более специфичные версии, у которых определены чёткие целиmap()
преобразовывает массив в новый массив. Каждый элемент массива преобразовывается с помощью переданной функцииfilter()
выбирает подмножество оригинального массива в новый массив. Критерии выбора описаны в переданной функцииreduce()
комбинирует элементы массива и переданное изначальное значение в результирующее значение