Функциональное программирование и его применение в JavaScript

В последнее время React и Redux продвинули в массы функциональное программирование, но не все освоили его. Давайте разбираться.

Когда функциональное программирование оправдано?

Прежде чем мы рассмотрим, что такое функциональное программирование, полезно будет узнать, когда оно чаще всего используется. Например, при преобразовании данных "один в один":

// Хранилище
type UserMap = {[userId: number]: User}
// Функциональный слой
type convertUserMapToArray: (userMap: UserMap) => User[];
// Компонент
type Component = ({ users: User[] }) => JSX.Element

В приведенном выше фрагменте кода представлены типы хранилища, компонента и функционального слоя между ними.

Код типа convertUserMapToArray, преобразует данные из хранилища, в формат, подходящий для пользовательского интерфейса.

Что такое функциональное программирование?

ФП – это процесс создания ПО при помощи "чистых" функций. Чистая функция – это функция без сайд-эффектов, имеющая одно входное и одно выходное значение.

Например, мы можем быть уверены, что 2 + 2 = 4, а 3 x 3 = 9.

Побочные эффекты (сайд-эффекты)

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

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

Рассмотрим несколько примеров побочных эффектов.

Мутация

Изменение переданного аргумента.

function pop(arr) {
  return arr.splice(0, 1);
}

const arr = [1,2,3,4];
pop(arr);
console.log(arr); // [2, 3, 4];

В приведенном выше примере изменяется значение arr по ссылке. В результате, мы не можем предугадать, что вернет эта функция.

Разделяемое состояние

Использование какой-либо формы глобального состояния функционального программирования.

// Разные значения при каждом вызове
let i = 0;
function increment() {
 return i++;
}

function decrement() {
  return i--;
}

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

Асинхронный код

Код, который не выполнится немедленно.

let i = 0;
function incrementAsync(obj) {
  setTimeout(() => {
    i++;
  }, 0)
}
incrementAsync();
console.log(i); // 0
console.log(i); // 1

Этот пункт заслуживает особого внимания. Часто нужно сделать что-то асинхронно, каким-либо образом обратиться к API, получить данные и т. д. Это отсылка к пункту о побочных эффектах – они должны быть изолированы, чтобы сделать код более предсказуемым и понятным.

Декларативный и императивный код

Декларативный код описывает, что он делает:

function ReactComponent({counter}) {
  return <span>{counter}</span>
}

Императивный код описывает, как он это делает:

function UpdateCounter({counter}) {
  document.getElementById('counter').innerHTML(
    `<span>${counter}<span>`
  );
}

Верхний блок написан на React с использованием декларативной библиотеки. Он сообщает: "Мы хотим счетчик на странице".

Нижний блок использует vanilla JS. Он явно находит ноду DOM и обновляет ее. Для маленького примера – это нормально, но в крупном проекте могут возникнуть проблемы с масштабированием. Код на React выглядит проще и компактнее, а с vanilla JS все обстоит иначе.

Примеры

Императивный код

function getFileMapById(files) {
  const fileMap = {};
  for (let i=0; i<files.length; i++) {
    const file = files[i];
    fileMap[file.id] = file;
  }
  return fileMap;
}

Декларативный код

function getFileMapById(files) {
  return lodash.keyBy(files, 'id');
}

Две данные функции выполняют одно действие – берут список файлов и возвращают словарь, где ключом является file.id.

Императивный код более неряшлив (8 строк кода, вместо 3), а из-за этого больше мест для появления ошибок.

Функциональные концепции

Разделение

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

Композиция

function sortFilesByName(files) {
  return lodash.sortBy(files, 'name');
}

function getPdfFiles(files) {
  return lodash.filter(files, {extension: PDF});
}

function getFileNames(files) {
  return lodash.map(files, 'name');
}

Все описанные функции выполняют преобразование аргумента. Первая – сортирует список файлов, вторая – возвращает массив файлов с расширением PDF, а последняя функция реализует преобразование списка 1:1 в его новую “версию”, просматривая список файлов и возвращая ключ каждого из них.

Неизменность

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

Memoization

Memoization – это метод оптимизации, используемый в основном для ускорения работы ПО путем сохранения результатов функций и возврата кэшированного результата при повторном вводе тех же данных.

Функции высокого порядка

В математике и информатике функцией высокого порядка называется функция, выполняющая по крайней мере одно из следующих действий:

  • принимает одну или несколько функций в качестве аргументов (т. е. процедурных параметров);
  • возвращает функцию в качестве результата.

Кажется сложным, но вы, скорее всего, уже использовали такие функции в своем коде когда использовании функциональное программирование, не осознавая этого.

Функция, принимающая функцию

fetch('user', {userId: 1}).then((response) => {
  persistUser(response);
})

Это просто обратный вызов – функция высокого порядка принимает в качестве аргумента анонимную функцию.

Функция, возвращающая функцию

function counterGenerator() {
  let i = 0;
  return function() {
    console.log(++i);
  }
}

const counter = counterGenerator();
counter(); // => 1
counter(); // => 2
counter(); // => 3

Еще одна штука, которую вы, вероятно, делали раньше, – это замыкания. В данном случае counterGenerator является функцией высокого порядка, поскольку возвращает функцию.

Функция, которая принимает и возвращает функцию

Последняя форма функции – симбиоз:

const killSiblingMemoized = lodash.memoize(killSibling);

const getSortedPDFFileNames = lodash.flow(
  getPdfFiles,
  getFileNames,
  lodash.sortBy
);

Здесь memoize и flow функции высокого порядка, принимают функцию (или несколько) в качестве аргументов и возвращают новую функцию.

Currying и функциональное программирование

Это метод конвертации функции, принимающей несколько аргументов (или кортеж аргументов) в последовательность функций с одним аргументом.

function sum(a, b, c) {
  return a + b + c;
}
const curriedSum = lodash.curry(sum);

curriedSum(1,2,3) // 6

const addFive = curriedSum(2,3);
addFive(7) // 12
addFive(8) // 13

const addOne = curriedSum(1)
addOne(2,3) // 6
const addThree = addOne(2);
addThree(3) // 6;
addThree(4) // 7;

Есть sum, принимающая аргументы a, b и c. Когда sum передается в lodash.curry (функция высокого порядка), она становится новой функцией и продолжает возвращать функции, пока a, b и c не будут обработаны.

Если на вход передать аргументы 1, 2, 3, то функция вернет 6. Если передать только 2, 3 – вернется новая функция, ожидающая еще одного аргумента.

Частичное применение

При частичном применении мы можем взять функцию и привязать к ней готовые аргументы оригинальной функции. Рассмотрим пример:

function learnSpell(spell, wizard) {
  return {
    ...wizard,
    spells: [
      ...wizard.spells,
      spell
    ],
  };
}

const learnExpelliarmus = lodash.partial(learnSpell, "Expelliarmus");
const learnExpectoPatronum = lodash.partial(learnSpell, "Expecto Patronum!");

let harry = {name: "Harry Potter", spells: []};
harry = learnExpelliarmus(harry);
// {name: "Harry Potter", spells: ["Expelliarmus"]}
harry = learnExpectoPatronum(harry);
// {name: "Harry Potter", spells: ["Expelliarmus", "Expecto Patronum"]}

Функция learn Spell, принимающая в параметрах заклинание и волшебника. Если эту функцию с заклинанием передать в lodash.partial, будет создана новая функция, которая обучит мага предопределенному заклинанию (learn Expelliarmus и learn Expecto Patronum).

Подобный пример был рассмотрен в разделе о композиции:

function sortFilesByName(files) {
  return lodash.sortBy(files, 'name');
}
function getPdfFiles(files) {
  return lodash.filter(files, {extension: PDF});
}
function getFileNames(files) {
  return lodash.map(files, 'name');
}

Данные функции частично передают аргумент функции lodash. Это эквивалентно такому блоку:

const sortFilesByName = lodash.partialRight(lodash.sortBy, 'name'));
const getPdfFiles = lodash.partialRight(lodash.filter, {extension: PDF}));
const getFileNames = lodash.partialRight(lodash.map, 'name');

Чего можно добиться оптимизацией

Это не попытка сократить строки кода – здесь все намного серьезнее:

  • сделать код более читабельным и простым;
  • упростить код для тестов;
  • сделать пользователей счастливее.

Оригинал

Другие материалы по теме:

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

eFusion
01 марта 2020

ТОП-15 книг по JavaScript: от новичка до профессионала

В этом посте мы собрали переведённые на русский язык книги по JavaScript – ...
admin
10 июня 2018

Лайфхак: в какой последовательности изучать JavaScript

Огромный инструментарий JS и тонны материалов по нему. С чего начать? Расск...