Функции
Спасибо за перевод Роман Ахмадуллин
Недавно я познакомился с замечательной серией статей "Thinking in Ramda", которые проясняют на простых и ясных примерах способы написания кода в функциональном стиле с использованием библиотеки Ramda.
Так как Apps Script использует движок JavaScript Rhino v1.7r3 мне придется переписать под него код приведенный в статьях. Используя следующие материалы:
Так-же попробую привести аналоги возможностей FP без использования библиотеки Ramda
3. Частичное применение (каррирование)
4. Декларативное программирование
Функции
Как следует из названия, функциональное программирование имеет много общего с функциями. Для нашей ситуации, мы определим функцию как кусочек переиспользуемого кода, который вызывается с количеством аргументов, равным нулю и более, и возвращает результат.
Это простая функция, написанная на JavaScript:
function double(x) { return x * 2 }
Некоторые языки идут дальше и предоставляют поддержку для функций как конструкций первого класса. Под «конструкциями первого класса» я подразумеваю, что функции могут использоваться таким же образом, как прочие значения. К примеру, вы можете:
— ссылаться на них в константах и переменных
— передавать их в качестве параметров в другие функции
— возвращать их как результат от других функций
JavaScript — один из подобных языков, и мы будем использовать это преимущество.
Чистые функции
При написании функциональных программ, вы в конце концов приходите к пониманию важности работы с так называемыми «чистыми» функциями.
Чистые функции — это функции, которые не имеют побочных эффектов. Они ничего не присваивают внешним переменным, они не уничтожают входные данные, не генерируют вывод, не читают и не пишут в базу данных, они не изменяют параметры, которые были им переданы, и так далее.
Основная идея заключается в том, что если вы вызываете функцию с теми же параметрами снова и снова, то вы всегда будете получать один и тот же результат.
Безусловно, вы можете делать различные дела с нечистыми функциями (и должны, если ваша программа делает что-то интересное), но для большей части кода вы желаете сохранить свои функции чистыми.
Неизменяемость
(или «иммутабельность», как часто выражаются фп'шники — прим. пер.)
Другая важная концепция в функциональном программировании — это «иммутабельность». Что это значит? «Иммутабельный» означает «неизменяемый».
Когда я работаю c иммутабельностью, после первичной инициализации значения или объекта, я уже не изменяю их вновь. Это значит, что вы не изменяете элементы в массиве или свойства объектов.
Если мне необходимо изменить что-то в массиве или объекте, — я возвращаю новую его копию с изменёнными значениями. В последующих постах мы поговорим об этом в подробностях.
Иммутабельность идёт рука об руку с чистыми функциями. Поскольку чистые функции не имеют права создавать побочные эффекты, они не имеют права изменять внешние структуры данных. Они вынуждены работать с данными в иммутабельном стиле.
С чего начать?
Самый простой путь начать мыслить в функциональной парадигме — начать заменять циклы на итерационные функции.
Мартин Флауер имеет набор прекрасных статей о «Потоках коллекций», которые показывают, как использовать эти функции и как отрефакторить существующий код в потоки обработки коллекций.
Обратите внимание, что все эти функции (за исключением 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)
Заключение
Начиная с данных итерирующих функций, вы можете уловить идею пробрасывания функций в другие функции. Возможно даже, что вы уже использовали это в других языках без понимания того, что вы занимались в этот момент функциональным программированием.