Мы рассмотрели, как работают методы JavaScript map() и filter(), в части 1 (JavaScript и функциональное программирование, часть 1: карта и фильтр). Теперь мы рассмотрим еще одну основу функционального программирования: композицию и каррирование.

Мы будем использовать RamdaJS для карри и компоновки наших функций. Если вы хотите поиграть с примерами из этой статьи на консоли DevTools, используйте следующий фрагмент для загрузки ramdajs:

var s = document.createElement(“SCRIPT”);
s.src = “https://cdn.jsdelivr.net/npm/[email protected]/dist/ramda.min.js";
document.head.appendChild(s);

Начнем с функциональной композиции:

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

Мы будем использовать R.compose для создания нашего пайплайна. Несколько вещей, которые нужно помнить о R.compose(), прежде чем мы углубимся в пример:

  1. Трубопровод течет справа налево. Сначала вызывается самая правая функция, а затем ее возвращаемое значение передается функции слева. Вывод последней функции будет результатом композиции.
  2. Только первая функция может иметь несколько значений, остальные функции могут принимать только один аргумент.

Рассмотрим следующий набор данных:

const data = [
 {
 “name”: “Avengers: Endgame”,
 “year”: “2019”,
 “genre”: “action”
 },
 {
 “name”: “Titanic”,
 “year”: “1997”,
 “genre”: “Romance”
 },
 {
 “name”: “Skyfall”,
 “year”: “2012”,
 “genre”: “action”
 },
 {
 “name”: “The Dark Knight”,
 “year”: “2008”,
 “genre”: “action”
 },
 {
 “name”: “Joker”,
 “year”: “2019”,
 “genre”: “action”
 },
 {
 “name”: “Star Wars: The Rise of Skywalker “,
 “year”: “2019”,
 “genre”: “action”
 }
]

Допустим, мы хотим сначала отфильтровать фильмы по году «2019», а затем по жанру «боевик».

Мы можем составить описанную выше операцию следующим образом:

const getActionMoviesFrom2019 = R.compose(R.filter(m=>m.genre == "action"),R.filter(m=>m.year== "2019"))
getActionMoviesFrom2019(data)

Мы определяем композицию под названием «getActionMoviesFrom2019» и вызываем ее в следующем операторе с нашим набором данных.

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

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

Хорошо, давайте перейдем к каррированию.

Каррирование происходит, когда вы вызываете функцию n-арности с менее чем n аргументами и возвращаете частично примененную функцию.

Например:

function add(num1, num2) {
return num1 + num2
}

Допустим, в данный момент у нас есть только «num1». Давайте применим «num1» к функции «add», пока мы ждем, пока «num2» станет доступным:

const addCurried = R.curry(add);

Это вернет функцию, которая будет принимать 2 аргумента.

Если бы мы вызвали addCurried с двумя аргументами, он вернул бы сумму этих двух чисел.

Однако, если мы вызовем addCurried только с одним аргументом, она вернет функцию, которая частично применит данный аргумент:

const addWith4 = addCurried(4)

Теперь, когда мы применяем второй аргумент к «addWith4», например:

const result = addWith4(5)

Он будет обрабатывать частично примененные 4 как «num1», а теперь предоставленные 5 как «num2» в первоначально каррированной функции «add()», что приведет к добавлению двух заданных аргументов.

Когда полезно карри? Когда вы работаете с данными, которые частично постоянны, а частично переменны. Например, рассмотрим вышеупомянутый набор данных фильмов. Что, если у него сотни тысяч записей. Например, вы знаете, что год «2019» останется постоянным для ваших запросов, но жанр может измениться. В этом случае вы можете просто применить метод фильтра с частично примененным годом «2019» следующим образом:

// define a curried function which takes partial values dataset, year, and genre
const getMoviesByGenreForYear = R.curry((data, year, genre) => {
  const filteredListByYear = data.filter(m => m.year == year)
  const filteredListByGenre = filteredListByYear.filter(m => m.genre == genre)
  return filteredListByYear
});

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

const getMoviesByGenreForYearWithData =  getMoviesByGenreForYear(data)

Затем мы применяем год «2019» к функции «getMoviesByGenreForYear()».

getMoviesFromYear2019For = getMoviesByGenreForYearWithData (“2019”)

Теперь мы можем запросить функцию getMoviesForYear2019For() для разных жанров, например:

getMoviesFromYear2019For("action")
getMoviesFromYear2019For("drama") 
getMoviesFromYear2019For("comedy")

Давайте посмотрим, как мы этого добились.

Сначала мы создали функцию, которая может принимать до 3 аргументов — набор данных, год выпуска фильма и его жанр. Затем мы предоставили ей набор данных (первый аргумент). Это дало нам другую функцию, к которой применен набор данных, и теперь она ожидает еще 2 аргумента. Поскольку нас интересуют фильмы 2019 года, мы применили это и получили функцию «getMoviesForYear2019()». К этой функции теперь применяются два наших аргумента. Ей просто нужен аргумент «жанр», и как только мы вызываем функцию с аргументом жанра, вся функция запускается со всеми тремя примененными аргументами и возвращает список, который мы хотим.

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

Имея в своем арсенале композицию и каррирование, вы получаете более эффективный и читаемый код. Более того, как только вы начнете решать свои проблемы с точки зрения функционального программирования, эффективность ваших алгоритмов резко повысится. Это, безусловно, имело место со мной с тех пор, как я начал программировать на Clojure — функциональном языке LISP для JVM.