Недавно я познакомился с замечательной серией статей "Thinking in Ramda", которые проясняют на простых и ясных примерах способы написания кода в функциональном стиле с использованием библиотеки Ramda. Эти статьи показались мне настолько прекрасными, что я не смог удержаться от того, чтобы не перевести их на русский язык. Надеюсь, что в этом будет польза для многих людей :) Давайте начнём перевод с первой вступительной статьи.
1. Первые шаги
2. Сочетаем функции
3. Частичное применение (каррирование)
4. Декларативное программирование
5. Бесточечная нотация
6. Неизменяемость и объекты
7. Неизменяемость и массивы
8. Линзы
9. Заключение
Данный пост — это начало серии статей «Мышление в стиле Ramda» о функциональном программировании.
Я буду использовать библиотеку Ramda в этих статьях, хотя многие из обсуждаемых идей применимы также к множеству других библиотек, таких как Underscore и Lodash, а также к другим языкам программирования.
Я буду придерживаться лёгкой, менее академической стороны функционального программирования. Это в основном потому что я хочу, чтобы серия была доступна большему числу людей, но также частично и потому что я сам не так близок к истинно функциональной дороге.
Я несколько раз затрагивал библиотеку Ramda для JavaScript в данном блоге:
— В "Используем Ramda с Redux" (я надеюсь также перевести и эту статью впоследствии — прим. пер.), я показал некоторые примеры того, как Ramda может быть использована в различных контекстах при написании Redux приложения.
— В "Используем Redux-api-middleware с Rails", я использовал Ramda для трансформации полезной нагрузки к запросам и возвращаемым ответам.
Я нашёл Ramda прекрасно спроектированной библиотекой, которая предоставляет множество инструментов для чистого и элегантного функционального программирования в JavaScript.
Если вы желаете поэкспериментировать с Ramda в процессе чтения этой серии статей, то на сайте Ramda имеется удобная браузерная песочница для ваших экспериментов.
Как следует из названия, функциональное программирование имеет много общего с функциями. Для нашей ситуации, мы определим функцию как кусочек переиспользуемого кода, который вызывается с количеством аргументов, равным нулю и более, и возвращает результат.
Это простая функция, написанная на JavaScript:
Вместе со стрелочными функциями из ES6, вы можете написать ту же самую функцию гораздо более кратко. Я упоминаю об этом сейчас, потому что мы будем использовать множество стрелочных функций по мере продвижения наших статей.
Некоторые языки идут дальше и предоставляют поддержку для функций как конструкций первого класса. Под «конструкциями первого класса» я подразумеваю, что функции могут использоваться таким же образом, как прочие значения. К примеру, вы можете:
— ссылаться на них в константах и переменных
— передавать их в качестве параметров в другие функции
— возвращать их как результат от других функций
JavaScript — один из подобных языков, и мы будем использовать это преимущество.
При написании функциональных программ, вы в конце концов приходите к пониманию важности работы с так называемыми «чистыми» функциями.
Чистые функции — это функции, которые не имеют побочных эффектов. Они ничего не присваивают внешним переменным, они не уничтожают входные данные, не генерируют вывод, не читают и не пишут в базу данных, они не изменяют параметры, которые были им переданы, и так далее.
Основная идея заключается в том, что если вы вызываете функцию с теми же параметрами снова и снова, то вы всегда будете получать один и тот же результат.
Безусловно, вы можете делать различные дела с нечистыми функциями (и должны, если ваша программа делает что-то интересное), но для большей части кода вы желаете сохранить свои функции чистыми.
(или «иммутабельность», как часто выражаются фп'шники — прим. пер.)
Другая важная концепция в функциональном программировании — это «иммутабельность». Что это значит? «Иммутабельный» означает «неизменяемый».
Когда я работаю c иммутабельностью, после первичной инициализации значения или объекта, я уже не изменяю их вновь. Это значит, что вы не изменяете элементы в массиве или свойства объектов.
Если мне необходимо изменить что-то в массиве или объекте, — я возвращаю новую его копию с изменёнными значениями. В последующих постах мы поговорим об этом в подробностях.
Иммутабельность идёт рука об руку с чистыми функциями. Поскольку чистые функции не имеют права создавать побочные эффекты, они не имеют права изменять внешние структуры данных. Они вынуждены работать с данными в иммутабельном стиле.
Самый простой путь начать мыслить в функциональной парадигме — начать заменять циклы на итерационные функции.
Если вы пришли с другого языка, который имеет эти функции (Ruby и Smalltalk лишь два примера), вы можете быть уже знакомы с ними.
Мартин Флауер имеет набор прекрасных статей о «Потоках коллекций», которые показывают, как использовать эти функции и как отрефакторить существующий код в потоки обработки коллекций.
Обратите внимание, что все эти функции (за исключением reject) доступны в Array.prototype, так что вам не нужна Ramda для того чтобы начать использовать их. Тем не менее, я буду использовать Ramda версии для согласованности с остальными статьями.
Вместо того чтобы писать явный цикл, попробуйте использовать функцию forEach вместо этого. Вот так:
forEach берёт функцию и массив, и вызывает эту функцию к каждому элементу массива.
В то время как forEach — это наиболее доступная из этих функций, она используется в наименьшей степени при выполнении функционального программирования. Она не возвращает значения, так что она в реальности используется только для вызова функций, которые имеют побочные эффекты.
Следующая наиболее важная функция, которую мы изучим — это map. Как и forEach, map применяет функцию к каждому элементу массива. Тем не менее, в отличии от forEach, map собирает результат применения это функции в новый массив и возвращает его.
Вот вам пример:
Он использует анонимную функцию, но мы можем использовать здесь и именованную функцию:
Теперь, давайте взглянем на filter и reject. Как следует из названия, filter выбирает элементы из массива, на основе некоторой функции. Вот пример:
filter применяет эту функцию (isEven в данном случае) к каждому элементу массива. Всякий раз, когда функция возвращает «правдивое» значение, соответствующий элемент включается в результат. И также всякий раз, когда функция возвращает «ложное» значение, соответствующий элемент исключается (фильтруется) из массива.
reject делает точно такую же вещь, но в обратном смысле. Она сохраняет элемент для каждой функции, которая вернёт ложное значение, и исключает элемент для тех функций, которые вернут истинное значение.
find применяет функцию к каждому элементу массива и возвращает первый элемент, для которого функция возвращает истинное значение.
reduce это немного более сложная чем другие функции, которые мы сегодня рассмотрели. Это стоит знать, но если у вас проблемы с пониманием сути её работы, не позволяйте этому останавливать вас. Вы можете пройти довольно долгий путь даже не понимая суть её работы.
reduce принимает функцию с двумя аргументами, изначальное значение и массив для работы с ним.
Первый аргумент, который будет передан функции, называется «аккумулятором», а вторым аргументом является значение итерируемого массива. Функция должна вернуть новое значение «аккумулятора».
Давайте взглянем на пример и затем разберём то, что в нём происходит:
Начиная с данных итерирующих функций, вы можете уловить идею пробрасывания функций в другие функции. Возможно даже, что вы уже использовали это в других языках без понимания того, что вы занимались в этот момент функциональным программированием.
Следующий пост в этой серии, "Сочетаем функции", покажет, как мы можем перейти к следующему шагу и начать совмещать функции в новых интересных вариантах.
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
- reduce вызывает функцию (add) с изначальным значением (5) на первом элементе массива (1). add возвращает новое значение аккумулятора (5 + 1 = 6).
- reduce снова вызывает add, это время нового значения аккумулятора (6), и следующего значения массива (2). add возвращает 8.
- reduce вызывает add снова с 8 и следующим значением (3), результат получается 11.
- reduce вызывает add в последний раз с 11 и последним значением массива (4), результатом является 15.
- reduce возвращает конечное аккумулируемое значение в качестве результата (15)
Заключение
Начиная с данных итерирующих функций, вы можете уловить идею пробрасывания функций в другие функции. Возможно даже, что вы уже использовали это в других языках без понимания того, что вы занимались в этот момент функциональным программированием.
В следующей серии
Следующий пост в этой серии, "Сочетаем функции", покажет, как мы можем перейти к следующему шагу и начать совмещать функции в новых интересных вариантах.