Хабрахабр

[Перевод] Мышление в стиле Ramda: Бесточечная нотация

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

Данный пост — это пятая часть серии статей о функциональном программировании под названием "Мышление в стиле Ramda".

В четвёртой части мы говорили о написании кода в декларативном стиле (объясняем компьютеру что нужно сделать) вместо императивного (говорим ему, как это сделать).

Вы могли заметить, что некоторые из функций, которые мы написали (forever21, drivingAge и water, к примеру) все принимают параметр, создают новую функцию и применяют эту функцию к параметру.

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

Бесточечная нотация

Существует два основных руководящих принципа в Ramda, о которых мы уже говорили в третьей части.

Передавать данные последними
2. 1. Каррировать все вещи

Я люблю думать о бесточечном коде как о "Данные? Эти два принципа ведут к стилю, который функциональные программисты называют "бесточечным". Здесь нигде нет данных". А где данные?

Он имеет такие заголовки как "Где мои данные?", "Ладно, всё! Есть один прекрасный пост Почему 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 $°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)
)

Это похоже на паттерн, который мы видели с age ранее, так давайте применим к этой ситуации похожую трансформацию: Обратите внимание, что books встречается лишь дважды: один раз как последний параметр в списке аргументов (данные идут последними!), и однажды в самом конце функции, когда мы применяем наш конвеер.

const titlesForYear = year => pipe( filter(publishedInYear(year)), map(book => book.title) )

Теперь у нас есть бесточечная версия titlesForYear. Оно работает!

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

Мы просто можем вызвать titlesForYear(2012). Если мы хотим использовать titlesForYear в конвеере, всё будет чудесно. На мой взгляд, оно того не стоит. Но если мы пожелаем использовать эту функцию отдельно, нам придётся вернуться к паттерну )(, который мы видели в предыдущем посте: 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)

Эта функция принимает person и применяет к ней две разных функции, объединяя результат с помощью ||. Давайте начнём с isCitizen. Как мы уже узнали во второй части, вместо этого мы можем использовать 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, чтобы знать, сколько аргументов им обычно нужно.

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

Мы можем утверждать, что это лишь академическое занятие, признанное для того чтобы дать функциональному программированию ещё один бейджик. Какое преимущество имеет бесточечный стиль? Тем не менее, я думаю, что у него всё же есть несколько реальных достоинств, даже не смотря на то, что вам придётся потратить время на то чтобы привыкнуть к нему:

  • Это делает программы более простыми и короткими. Не всегда это хорошо, но тем не менее.
  • Это делает алгоритмы чище. Фокусируясь только на функциях, которые объединяются, мы получаем больше смысла о том, что происходит, не акцентируя внимание на обрабатываемых аргументах с данными.
  • Это заставляет нас больше думать о трансформациях, чем о данных, которые трансформируются.
  • Это помогает нам думать о наших функциях как о необльших строительных блоках, которые могут работать с различными видами данных, примерно как думать об операциях с определённым типом данных. Когда мы опускаем аргумент с данными, это позволяет нам становиться более креативными.

Заключение

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

Далее

У нас всё ещё имеется код, который написан в императивном стиле. В наших примерах мы не могли отрефакторить всё к бесточечному стилю. Большинство этого кода работает с объектами и массивами.

И что насчёт иммутабельности? Нам нужно найти декларативные пути для работы с объектами и массивами. Как мы будем манипулировать объектами и массивами в иммутабельном стиле?

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть