Создание цепочек из функциональных инструментов

Васильев Андрей Михайлович

2021

Содержание

Мы изучили базовые инструменты функционального итерирования по массивам. Однако реальные задачи зачастую сложнее и требуют применения сразу набора из данных методов. В рамках данной лекции рассмотрим подходы к созданию цепочек из функциональных инструментов

В рамках цепочек каждый шаг прост и понятен, но вместе они могут решать сложные задачи по обработке данных

Запрос: наибольшие покупки лучших клиентов

Команда взаимодействия предполагает, что лояльные клиенты совершают также и наибольшие покупки. Надо узнать стоимость наибольшей покупки от лояльных клиентов

Отдельные задачи мы можем успешно решать:

Данную задачу можно решить путём формирования цепочки из функций

Последовательность действий для решения задачи

  1. Выполнить фильтрацию клиентов для нахождения лояльных
  2. Для каждого оставшегося клиента найти стоимость наибольшей покупки
    1. Преобразовать список покупок в их стоимость
    2. Найти максимальную стоимость

Реализация решения

Начнём с определения функции:

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() и max() очень похожи. То есть в теории могут иметь очень похожий код

Предположите, что вам необходимо написать одну функцию в терминах другой функции

  1. Какую функцию следует использовать и почему?
  2. Напишите код обеих функций
  3. Сформируйте граф вызовов для этих функций
  4. Какая из этих двух функций более общая?

  1. max() должна быть реализована в терминах maxKey()

  2. Можно реализовать max() с помощью maxKey(), передавая ей функцию возвращающую свой аргумент

    function max(array, init) {
      return maxKey(array, init, function(x) {
        return x;
      });
    }
  3. Граф вызовов: max() -> maxKey() -> reduce() -> forEach() -> for loop

  4. 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;
}

Можно ли добиться повторного использования написанного кода?

Улучшение цепочек № 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;
}

Сравнение двух подходов

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()? Что его позиция говорит о возможности повторного применения, тестирования и поддержке?

Сводка подсказок по преобразованию кода

Создавайте данные для обхода

Функциональные инструменты ориентированы на использование с массивами данных. Необходимо явно выделить данные массиве в коде

Выполняйте обработку массива целиком

Необходимо определить функцию высшего порядка, которая наиболее точно подходит для решения задачи путём обработки всего массива

Делайте много маленьких шагов

Если кажется, что код стал выполнять много действий, то выделение небольших и понятных последовательностей действий может помочь в выполнении преобразования

Заменяйте условия вызовом reduce()

Если в рамках цикла for присутствует условие, то оно зачастую направлено на пропуск элементов. Такие условия можно удобно заменить на вызов reduce()

Выделяйте функции-помощники

Помимо 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 они присутствуют, хотя и не проверяются во время компиляции

Используйте информацию о типах данных для достижения результата

Используя эту информацию можно достаточно быстро разобраться в назначении кода

Другие функциональные инструменты

Рассмотрим другие функции, которые могут быть полезны для решения задач. Это только небольшой набор методов из большого арсенала

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: ряд библиотека

Haskel Prelude

Данный стандартный модуль языка Haskell содержит небольшой набор функциональных инструментов, которые предоставляют мощные возможности для функционального программирования. Документация, краткий обзор

Java

Начиная с 8 выпуска языка в язык Java стали добавлять расширенную поддержку функциональных инструментов

Java: ряд библиотек и языков

Ruby

Язык Ruby предлагает большое количество функциональных инструментов:

Стандартная библиотека 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);
  }
}

Реализация отмены действия

На предыдущем этапе мы реализовали создание корзины из списка товаров. Однако этого недостаточно, чтобы реализовать отмену действия, т.к. оно может быть как добавлением, так и удалением

Необходимо расширить массив информацией о действиях пользователя

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));
});

Практика

Теперь необходимо объединить все три предыдущие практики в рамках одно функционирующей системы. На вход ей подаётся список сотрудников, из которых нужно сформировать хорошо функционирующую команду

Помимо разработанных методов также потребуются методы

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());
}

Заключение

В данной лекции мы рассмотрели процесс построения цепочек из функций для обработки данных. На каждом этапе происходит преобразование данных в форму, которая чуть ближе к желаемому финальному результату