Pull to refresh

Мышление в стиле Ramda: Сочетаем функции

Reading time5 min
Views13K
Original author: Randy Coulman
Данный пост — это вторая часть серии статей о функциональном программировании под названием "Мышление в стиле Ramda".

1. Первые шаги
2. Сочетаем функции
3. Частичное применение (каррирование)
4. Декларативное программирование
5. Бесточечная нотация
6. Неизменяемость и объекты
7. Неизменяемость и массивы
8. Линзы
9. Заключение

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

Простые комбинации


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

Ramda предоставляет несколько функций для выполнения простых комбинаций. Давайте взглянем на парочку из них:

Complement


(прим. пер.: если кто-то знает — напишите пожалуйста в комментах, причём здесь слово «комплемент», когда речь идёт об аналоге !(expr) из императивного программирования?).

В прошлом посте мы использовали find для нахождения первого чётного числа в списке:

const isEven = x => x % 2 === 0
 
find(isEven, [1, 2, 3, 4]) // --> 2

Если бы мы пожелали найти первое нечётное число, мы бы могли написать функцию isOdd и использовать её. Но мы также знаем, что любое чётное число не является нечётным. Давайте переиспользуем функцию isEven.

Ramda предоставляет complement, функцию высшего порядка, которая берёт другую функцию и возвращает новую функцию, которая возвращает истину, когда оригинальная функция возвращает ложное значение, и ложь, когда оригинальная функция возвращает истинное значение.

const isEven = x => x % 2 === 0
 
find(complement(isEven), [1, 2, 3, 4]) // --> 1

Ещё лучше — дать функции-комплементу собственное название, чтобы иметь возможность переиспользовать её:

const isEven = x => x % 2 === 0
const isOdd = complement(isEven)
 
find(isOdd, [1, 2, 3, 4]) // --> 1

Обратите внимание, что complement реализует ту же самую идею, что и оператор ! для значений в императивных языках программирования.

Both / Either


Представим, что мы работаем над системой голосований. Имея человека, мы хотели бы иметь возможность определить, имеет ли это лицо право на голос. Основываясь на наших текущих знаниях, человек должен быть не моложе 18 лет и должен быть гражданином страны, чтобы иметь возможность голосовать. Кто-то является гражданином этой страны с рождения, а кто-то стал им в результате натурализации.

const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY
const wasNaturalized = person => Boolean(person.naturalizationDate)
const isOver18 = person => person.age >= 18
 
const isCitizen = person => wasBornInCountry(person) || wasNaturalized(person)
 
const isEligibleToVote = person => isOver18(person) && isCitizen(person)

То, что мы написали выше — работает, но Ramda предлагает несколько удобных функций, которые помогут сделать наш код немного более чистым.

Функция both берёт две других функции и возвращает новую функцию, которая вернёт true, если обе функции вернут правдивое значение при применении к ней аргументов, и false в противном случае.

either берёт две другие функции и возвращает новую функцию, которая вернёт true, если любая функция возвращает правдивое значение с применёнными к ней аргументами, и false в ином случае.

Используя эти две функции, мы можем упростить функции isCitizen и isEligibleToVote:

const isCitizen = either(wasBornInCountry, wasNaturalized)
const isEligibleToVote = both(isOver18, isCitizen)

Обратите внимание, что both, по сути, реализует ту же самую идею, что и оператор && (и) для значений, а either реализует такую же идею для функций, как оператор || (или) для значений.

Ramda также предоставляет такие методы как allPass и anyPass, которые берут массив с любым количеством функций. Как подсказывают их имена, allPass работает подобно both, а anyPass подобно either.

Конвейер


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

const multiply = (a, b) => a * b
const addOne = x => x + 1
const square = x => x * x
 
const operate = (x, y) => {
  const product = multiply(x, y)
  const incremented = addOne(product)
  const squared = square(incremented)
 
  return squared
}
 
operate(3, 4) // => ((3 * 4) + 1)^2 => (12 + 1)^2 => 13^2 => 169

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

Ramda предоставляет функцию pipe, которая берёт список одной и более функций и возвращает новую функцию.

Новая функция принимает такое же количество аргументов, что и первая. Далее она «передаёт по конвееру» эти аргументы через каждую функцию в списке. Она применяет к первой функции полученные аргументы, передаёт её результат во вторую функцию, и так далее. Результатом последней функции является результат прохождения всего конвеера.

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

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

const operate = pipe(
  multiply,
  addOne,
  square
)

Когда мы вызываем operate(3, 4), pipe функция передаёт 3 и 4 функции multiply, получая в итоге 12. Далее она передаёт 12 в addOne, которая вернёт 13. И далее она передаст 13 в функцию square, который вернёт 169, и это будет финальным результатом всего конвеера.

Compose


Другой способ, которым мы могли бы написать нашу оригинальную функцию конвеера — это написать его в одну строку:

const operate = (x, y) => square(addOne(multiply(x, y)))

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

compose работает точно также как и pipe, за исключением того, что он применяет функции справа налево, а не слева направо. Напишем нашу функцию operate с помощью compose:

const operate = compose(
  square,
  addOne,
  multiply
)

Это точно такой же конвеер, как и вышесозданный, но его функции находятся в обратном порядке. По факту, функция compose от Ramda написана на принципах конвеера.

Я всегда думаю о compose следующим образом: compose(f, g)(value) эквивалентно f(g(value)).

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

Compose или pipe?


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

Я ещё не разработал хорошее правило, когда я предпочитаю compose, а когда — pipe. Посколько они, по сути, эквивалентны в Ramda, вероятно, не имеет значения, какой из них вы выберете. Просто используйте то, что больше подходет к вашей ситуации.

Заключение


Объединяя несколько функций определённым образом, мы можем начать писать другие более мощные функции.

Возможно, вы заметили, что в основном мы игнорировали аргументы, когда мы сочетали функции. Мы передаём аргументы лишь тогда, когда мы, наконец, вызываем полученную функцию-конвеер.

Это одна из основ функционального программирования и мы ещё будем много говорить об этом в следующей статьей этой серии, «Частичное применение (каррирование)». Также мы поговорим о том, как сочетать функции, которые принимают больше одного аргумента.
Tags:
Hubs:
+11
Comments9

Articles

Change theme settings