javascript

Мышление в стиле Ramda: первые шаги

  • среда, 14 февраля 2018 г. в 03:16:56
https://habrahabr.ru/post/348868/
  • Функциональное программирование
  • JavaScript


Недавно я познакомился с замечательной серией статей "Thinking in Ramda", которые проясняют на простых и ясных примерах способы написания кода в функциональном стиле с использованием библиотеки Ramda. Эти статьи показались мне настолько прекрасными, что я не смог удержаться от того, чтобы не перевести их на русский язык. Надеюсь, что в этом будет польза для многих людей :) Давайте начнём перевод с первой вступительной статьи.

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

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

Я буду использовать библиотеку Ramda в этих статьях, хотя многие из обсуждаемых идей применимы также к множеству других библиотек, таких как Underscore и Lodash, а также к другим языкам программирования.

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

Ramda


Я несколько раз затрагивал библиотеку Ramda для JavaScript в данном блоге:

— В "Используем Ramda с Redux" (я надеюсь также перевести и эту статью впоследствии — прим. пер.), я показал некоторые примеры того, как Ramda может быть использована в различных контекстах при написании Redux приложения.

— В "Используем Redux-api-middleware с Rails", я использовал Ramda для трансформации полезной нагрузки к запросам и возвращаемым ответам.

Я нашёл Ramda прекрасно спроектированной библиотекой, которая предоставляет множество инструментов для чистого и элегантного функционального программирования в JavaScript.

Если вы желаете поэкспериментировать с Ramda в процессе чтения этой серии статей, то на сайте Ramda имеется удобная браузерная песочница для ваших экспериментов.

Функции


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

Это простая функция, написанная на JavaScript:

function double(x) {
  return x * 2
}

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

const double = x => x * 2

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

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

JavaScript — один из подобных языков, и мы будем использовать это преимущество.

Чистые функции


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

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

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

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

Неизменяемость


(или «иммутабельность», как часто выражаются фп'шники — прим. пер.)

Другая важная концепция в функциональном программировании — это «иммутабельность». Что это значит? «Иммутабельный» означает «неизменяемый».

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

Если мне необходимо изменить что-то в массиве или объекте, — я возвращаю новую его копию с изменёнными значениями. В последующих постах мы поговорим об этом в подробностях.

Иммутабельность идёт рука об руку с чистыми функциями. Поскольку чистые функции не имеют права создавать побочные эффекты, они не имеют права изменять внешние структуры данных. Они вынуждены работать с данными в иммутабельном стиле.

С чего начать?


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

Если вы пришли с другого языка, который имеет эти функции (Ruby и Smalltalk лишь два примера), вы можете быть уже знакомы с ними.

Мартин Флауер имеет набор прекрасных статей о «Потоках коллекций», которые показывают, как использовать эти функции и как отрефакторить существующий код в потоки обработки коллекций.

Обратите внимание, что все эти функции (за исключением reject) доступны в Array.prototype, так что вам не нужна Ramda для того чтобы начать использовать их. Тем не менее, я буду использовать Ramda версии для согласованности с остальными статьями.

forEach


Вместо того чтобы писать явный цикл, попробуйте использовать функцию forEach вместо этого. Вот так:

// Замените это:
for (const value of myArray) {
  console.log(value)
}
 
// на это:
forEach(value => console.log(value), myArray)

forEach берёт функцию и массив, и вызывает эту функцию к каждому элементу массива.

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

map


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

Вот вам пример:

map(x => x * 2, [1, 2, 3])  // --> [2, 4, 6]

Он использует анонимную функцию, но мы можем использовать здесь и именованную функцию:

const double = x => x * 2
 
map(double, [1, 2, 3])


filter/reject


Теперь, давайте взглянем на filter и reject. Как следует из названия, filter выбирает элементы из массива, на основе некоторой функции. Вот пример:

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

filter применяет эту функцию (isEven в данном случае) к каждому элементу массива. Всякий раз, когда функция возвращает «правдивое» значение, соответствующий элемент включается в результат. И также всякий раз, когда функция возвращает «ложное» значение, соответствующий элемент исключается (фильтруется) из массива.

reject делает точно такую же вещь, но в обратном смысле. Она сохраняет элемент для каждой функции, которая вернёт ложное значение, и исключает элемент для тех функций, которые вернут истинное значение.

reject(isEven, [1, 2, 3, 4]) // --> [1, 3]

find


find применяет функцию к каждому элементу массива и возвращает первый элемент, для которого функция возвращает истинное значение.

find(isEven, [1, 2, 3, 4]) // --> 2


reduce


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

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

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

Давайте взглянем на пример и затем разберём то, что в нём происходит:

const add = (accum, value) => accum + value
 
reduce(add, 5, [1, 2, 3, 4]) // --> 15

  1. reduce вызывает функцию (add) с изначальным значением (5) на первом элементе массива (1). add возвращает новое значение аккумулятора (5 + 1 = 6).
  2. reduce снова вызывает add, это время нового значения аккумулятора (6), и следующего значения массива (2). add возвращает 8.
  3. reduce вызывает add снова с 8 и следующим значением (3), результат получается 11.
  4. reduce вызывает add в последний раз с 11 и последним значением массива (4), результатом является 15.
  5. reduce возвращает конечное аккумулируемое значение в качестве результата (15)


Заключение


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

В следующей серии


Следующий пост в этой серии, «Сочетаем функции», покажет, как мы можем перейти к следующему шагу и начать совмещать функции в новых интересных вариантах.