1. Первые шаги
2. Сочетаем функции
3. Частичное применение (каррирование)
4. Декларативное программирование
5. Бесточечная нотация
6. Неизменяемость и объекты
7. Неизменяемость и массивы
8. Линзы
9. Заключение
Данный пост — это пятая часть серии статей о функциональном программировании под названием "Мышление в стиле Ramda".
В четвёртой части мы говорили о написании кода в декларативном стиле (объясняем компьютеру что нужно сделать) вместо императивного (говорим ему, как это сделать).
Вы могли заметить, что некоторые из функций, которые мы написали (forever21
, drivingAge
и water
, к примеру) все принимают параметр, создают новую функцию и применяют эту функцию к параметру.
Это очень распространённый паттерн в функциональном программировании, и Ramda здесь в очередной раз предоставляет нам утилиты для того чтобы ещё немного очистить наш код.
Бесточечная нотация
Существует два основных руководящих принципа в Ramda, о которых мы уже говорили в третьей части.
1. Передавать данные последними
2. Каррировать все вещи
Эти два принципа ведут к стилю, который функциональные программисты называют "бесточечным". Я люблю думать о бесточечном коде как о "Данные? А где данные? Здесь нигде нет данных".
Есть один прекрасный пост Почему Ramda?, который отлично иллюстрирует стиль бесточечной нотации. Он имеет такие заголовки как "Где мои данные?", "Ладно, всё! Я могу увидеть немного данных?" и "Мне просто нужны мои данные, спасибо".
У нас пока нет инструментов, необходимых для того, чтобы все наши примеры стали абсолютно бесточечными, но мы уже можем начать кое-что делать.
Давайте снова взглянем на forever21
:
const forever21 = age => ifElse(gte(__, 21), always(21), inc)(age)
Обратите внимание, что age
встречается лишь дважды: один раз в списке аргументов и один раз в самом конце функции, когда мы применяем функцию, которая возвращается вызовом ifElse
.
Если мы будем внимательны при работе с Ramda, мы заметим этот паттерн в множестве мест. Это почти всегда означает, что здесь есть возможность сконвертировать функцию в бесточечный стиль.
Давайте посмотрим как это будет выглядеть:
const forever21 = ifElse(gte(__, 21), always(21), inc)
И, пуф! Мы только что сделали так, что age
пропал. Бесточечная нотация. Обратите внимание, что здесь нет различий в поведении между этими двумя версиями функций. Этот код всё ещё возвращает функцию, которая получит возраст, но теперь мы не указываем явно параметр age
.
Мы точно также можем сделать такие же штуки с alwaysDrivingAge
и water
.
В последний раз alwaysDrivingAge
выглядел так:
const alwaysDrivingAge = age => ifElse(lt(__, 16), always(16), identity)(age)
Мы можем применить к нему подобную трансформацию для того чтобы сделать его бесточечным:
const alwaysDrivingAge = when(lt(__, 16), always(16))
А такой мы оставили функцию water
:
const water = temperature => cond([
[equals(0), always('water freezes at 0°C')],
[equals(100), always('water boils at 100°C')],
[T, temp => `nothing special happens at ${temp}°C`]
])(temperature)
А вот и её бесточечный аналог:
const water = cond([
[equals(0), always('water freezes at 0°C')],
[equals(100), always('water boils at 100°C')],
[T, temp => `nothing special happens at ${temp}°C`]
])
Мульти-аргументные функции
Что насчёт функций, которые принимает больше одного аргумента? Давайте вернёмся назад к функции titlesForYear
из третьей части.
const titlesForYear = curry((year, books) =>
pipe(
filter(publishedInYear(year)),
map(book => book.title)
)(books)
)
Обратите внимание, что books
встречается лишь дважды: один раз как последний параметр в списке аргументов (данные идут последними!), и однажды в самом конце функции, когда мы применяем наш конвеер. Это похоже на паттерн, который мы видели с age
ранее, так давайте применим к этой ситуации похожую трансформацию:
const titlesForYear = year =>
pipe(
filter(publishedInYear(year)),
map(book => book.title)
)
Оно работает! Теперь у нас есть бесточечная версия titlesForYear
.
Честно говоря, возможно, я не захотел бы использовать бесточечную версию этой функции, потому что JavaScript не имеет соглашения по вызову серии одно-аргументных функций, что уже обсуждалось в предыдущих постах.
Если мы хотим использовать titlesForYear
в конвеере, всё будет чудесно. Мы просто можем вызвать titlesForYear(2012)
. Но если мы пожелаем использовать эту функцию отдельно, нам придётся вернуться к паттерну )(
, который мы видели в предыдущем посте: titlesForYear(2012)(books)
. На мой взгляд, оно того не стоит.
Но в любое время, когда я имею одно-аргументную функцию, которая следует (или может быть отрефакторена для следования) вышенаписанному паттерну, — я почти всегда делаю её бесточечной.
Рефакторим в бесточечный стиль
Будут встречаться такие ситуации, когда наши функции не будут следовать этому паттерну. Мы можем начать работать с данными несколько раз в одной функции.
Есть несколько подобных примеров из второй части. В тех примерах мы отрефакторили наш код для того чтобы скомбинировать функции, используя такие штуки как both
, either
, pipe
и compose
. Как только мы закончили с этим, приведение этих функций к бесточечным стало довольно простым делом.
Давайте посмотрим на метод isEligibleToVote
. Вот с чего мы начинали:
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)
Давайте начнём с isCitizen
. Эта функция принимает person
и применяет к ней две разных функции, объединяя результат с помощью ||
. Как мы уже узнали во второй части, вместо этого мы можем использовать either
для объединения двух функций в новую функцию и последующего применения её к person
.
const isCitizen = person => either(wasBornInCountry, wasNaturalized)(person)
Мы можем проделать подобные штуки с isEligibleToVote
с помощью both
:
const isEligibleToVote = person => both(isOver18, isCitizen)(person)
Теперь, когда мы закончили с этим рефакторингом, мы можем увидеть что обе этих наших функции следуют паттерну, о котором мы говорили ранее: person
упоминается дважды, однажды как аргумент функции и однажды в самом конце применения наших скомбинированных функций к ней. Теперь мы можем преобразовать их в бесточечный стиль:
const isCitizen = either(wasBornInCountry, wasNaturalized)
const isEligibleToVote = both(isOver18, isCitizen)
Почему?
Бесточечный стиль требует некоторого времени для того чтобы привыкнуть к нему. Возможно, будет трудно адаптироваться к отсутствующим везде аргументам с данными. Это также важно для того чтобы ознакомиться с функциями Ramda, чтобы знать, сколько аргументов им обычно нужно.
Но когда однажды вы их усвоите, они станут очень мощными при необходимости создавать наборы небольших бесточечных функций, комбинируемые различными интересными способами.
Какое преимущество имеет бесточечный стиль? Мы можем утверждать, что это лишь академическое занятие, признанное для того чтобы дать функциональному программированию ещё один бейджик. Тем не менее, я думаю, что у него всё же есть несколько реальных достоинств, даже не смотря на то, что вам придётся потратить время на то чтобы привыкнуть к нему:
- Это делает программы более простыми и короткими. Не всегда это хорошо, но тем не менее.
- Это делает алгоритмы чище. Фокусируясь только на функциях, которые объединяются, мы получаем больше смысла о том, что происходит, не акцентируя внимание на обрабатываемых аргументах с данными.
- Это заставляет нас больше думать о трансформациях, чем о данных, которые трансформируются.
- Это помогает нам думать о наших функциях как о необльших строительных блоках, которые могут работать с различными видами данных, примерно как думать об операциях с определённым типом данных. Когда мы опускаем аргумент с данными, это позволяет нам становиться более креативными.
Заключение
Бесточечный стиль, также известный как молчаливое программирование, может сделать наш код чище и проще для размышления о нём. Производя рефакторинг нашего кода для объединения всех наших трансформаций в единую функцию, мы заканчиваем маленькими строительными блоками, которые могут использоваться в множестве мест.
Далее
В наших примерах мы не могли отрефакторить всё к бесточечному стилю. У нас всё ещё имеется код, который написан в императивном стиле. Большинство этого кода работает с объектами и массивами.
Нам нужно найти декларативные пути для работы с объектами и массивами. И что насчёт иммутабельности? Как мы будем манипулировать объектами и массивами в иммутабельном стиле?
Следующий пост данной серии, “Неизменяемость и Объекты” будет обсуждать, как мы можем работать с объектами в функциональном и иммутабельном стиле. После этого должен будет выйти пост “Неизменяемость и Массивы”, в котором будет обсуждаться то же самое по отношению к массивам.
Автор: Роман Ахмадуллин