Данный пост — это вторая часть серии статей о функциональном программировании под названием "Мышление в стиле Ramda".
1. Первые шаги
2. Сочетаем функции
3. Частичное применение (каррирование)
4. Декларативное программирование
5. Бесточечная нотация
6. Неизменяемость и объекты
7. Неизменяемость и массивы
8. Линзы
9. Заключение
В первой части я представил Ramda и некоторые основополагающие идеи от функционального программирования, такие как функции, чистые функции и иммутабельность. Далее я предположил, что хорошим местом для начала являются итерационные функции, такие как forEach, map, select и их друзья.
Как только мы усвоили идею прокидывания функций в другие функции, мы можем начать искать ситуации, в которых мы пожелаем объединить несколько функций вместе.
Ramda предоставляет несколько функций для выполнения простых комбинаций. Давайте взглянем на парочку из них:
(прим. пер.: если кто-то знает — напишите пожалуйста в комментах, причём здесь слово «комплемент», когда речь идёт об аналоге !(expr) из императивного программирования?).
В прошлом посте мы использовали find для нахождения первого чётного числа в списке:
Если бы мы пожелали найти первое нечётное число, мы бы могли написать функцию isOdd и использовать её. Но мы также знаем, что любое чётное число не является нечётным. Давайте переиспользуем функцию isEven.
Ramda предоставляет complement, функцию высшего порядка, которая берёт другую функцию и возвращает новую функцию, которая возвращает истину, когда оригинальная функция возвращает ложное значение, и ложь, когда оригинальная функция возвращает истинное значение.
Ещё лучше — дать функции-комплементу собственное название, чтобы иметь возможность переиспользовать её:
Обратите внимание, что complement реализует ту же самую идею, что и оператор ! для значений в императивных языках программирования.
Представим, что мы работаем над системой голосований. Имея человека, мы хотели бы иметь возможность определить, имеет ли это лицо право на голос. Основываясь на наших текущих знаниях, человек должен быть не моложе 18 лет и должен быть гражданином страны, чтобы иметь возможность голосовать. Кто-то является гражданином этой страны с рождения, а кто-то стал им в результате натурализации.
То, что мы написали выше — работает, но Ramda предлагает несколько удобных функций, которые помогут сделать наш код немного более чистым.
Функция both берёт две других функции и возвращает новую функцию, которая вернёт true, если обе функции вернут правдивое значение при применении к ней аргументов, и false в противном случае.
either берёт две другие функции и возвращает новую функцию, которая вернёт true, если любая функция возвращает правдивое значение с применёнными к ней аргументами, и false в ином случае.
Используя эти две функции, мы можем упростить функции isCitizen и isEligibleToVote:
Обратите внимание, что both, по сути, реализует ту же самую идею, что и оператор && (и) для значений, а either реализует такую же идею для функций, как оператор || (или) для значений.
Ramda также предоставляет такие методы как allPass и anyPass, которые берут массив с любым количеством функций. Как подсказывают их имена, allPass работает подобно both, а anyPass подобно either.
Иногда мы хотим применить несколько функций к некоторым данным в конвейерном стиле. Скажем, мы могли бы взять два числа, умножить их вместе, добавить единицу и возвести в квадрат результат. Вы можем написать что-то вроде этого:
Обратите внимание, что каждая операция применяется к результату предыдущей операции в конвеере.
Ramda предоставляет функцию pipe, которая берёт список одной и более функций и возвращает новую функцию.
Новая функция принимает такое же количество аргументов, что и первая. Далее она «передаёт по конвееру» эти аргументы через каждую функцию в списке. Она применяет к первой функции полученные аргументы, передаёт её результат во вторую функцию, и так далее. Результатом последней функции является результат прохождения всего конвеера.
Обратите внимание, что все функции после первой должны принимать лишь один аргумент.
Зная это, мы можем использовать функцию pipe для упрощения нашего конвеера.
Когда мы вызываем operate(3, 4), pipe функция передаёт 3 и 4 функции multiply, получая в итоге 12. Далее она передаёт 12 в addOne, которая вернёт 13. И далее она передаст 13 в функцию square, который вернёт 169, и это будет финальным результатом всего конвеера.
Другой способ, которым мы могли бы написать нашу оригинальную функцию конвеера — это написать его в одну строку:
Это гораздо более компактно, но несколько сложнее для чтения. В этой форме, однако, он может быть переписан с использованием функции compose из Ramda.
compose работает точно также как и pipe, за исключением того, что он применяет функции справа налево, а не слева направо. Напишем нашу функцию operate с помощью compose:
Это точно такой же конвеер, как и вышесозданный, но его функции находятся в обратном порядке. По факту, функция compose от Ramda написана на принципах конвеера.
Я всегда думаю о compose следующим образом: compose(f, g)(value) эквивалентно f(g(value)).
Обратите внимание, что как и с pipe, все функции, за исключением последней, должны принимать лишь один аргумент.
Я думаю, что pipe, вероятно, легче для понимания, когда вы приходите с более императивного поля деятельности, поскольку вы привыкли читать функции слева направо. Но compose, с другой стороны, намного больше похож на вызов нескольких вложенных функций, как я писал выше.
Я ещё не разработал хорошее правило, когда я предпочитаю compose, а когда — pipe. Посколько они, по сути, эквивалентны в 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, вероятно, не имеет значения, какой из них вы выберете. Просто используйте то, что больше подходет к вашей ситуации.
Заключение
Объединяя несколько функций определённым образом, мы можем начать писать другие более мощные функции.
Возможно, вы заметили, что в основном мы игнорировали аргументы, когда мы сочетали функции. Мы передаём аргументы лишь тогда, когда мы, наконец, вызываем полученную функцию-конвеер.
Это одна из основ функционального программирования и мы ещё будем много говорить об этом в следующей статьей этой серии, «Частичное применение (каррирование)». Также мы поговорим о том, как сочетать функции, которые принимают больше одного аргумента.
Neiromaster
Complement — отрицание (булева выражения или логической переменной). Это перевод именно в программировании.
saggid Автор
Это да, это понятно… Только обычный человеческий смысл этого слова какой-то совсем другой, и это сбивает меня с толку ) Обычно есть всё-таки нормальная логическая связь терминов из языков программирования с терминами из нашего реального мира.
Vadem
Complement так и переводится — дополнение.
Комплимент по английски — compliment.
saggid Автор
Так и переводится, "дополнение", всё верно. И как вы логически соотносите "дополнение" и "отрицание"? Это синонимы, или, может, термины, близкие друг к другу по значению? Я лично под "отрицанием" понимаю одно, а под "дополнением" — другое. А комплимент здесь причём?
Vadem
Никак не соотносил. В теории множеств есть термин дополнение. Он означает множество элементов не принадлежащих данному. Термин понятный и привычный любому программисту. Очевидно, что именно он и имеется ввиду. Не знаю причём тут отрицание.
saggid Автор
Логику работы этой функции в фп-языках более правильно было бы описать как "отрицание" всё-таки. Пока что я так понимаю. До сих пор не могу уловить, каким образом её можно было назвать комплементом. В этом и весь вопрос был.
qw1
Есть очевидное соответствие теоретико-множественных операций (объединение, пересечение, дополнение) и булевских (дизъюнкция, конъюнкция, отрицание). Характеристическая функция множества-дополнения это отрицание характеристической функции исходного множества.
saggid Автор
Спасибо, теперь и для таких тупых как я понятно, откуда всё это)
Картинка из инета для наглядности:
Оно конечно чисто со смысловой стороны меня всё равно до сих пор никак не удовлетворяет, я не вижу логики называть "дополнением" и то, что происходит на картинке… Ну теперь хотя-бы понятно, откуда растут корни )
qw1
«Дополнение A» не следует путать с «дополнением A до B» (она же — разность множеств).
http://ru.math.wikia.com/wiki/Дополнение (теория множеств)
Дополнение с одним аргументом — полный аналог булевской инверсии —
habrastorage.org/webt/z-/oo/rd/z-oordxtw3g63jhsyfefk8yqsdi.png