Функции

Функции



Спасибо за перевод Роман Ахмадуллин

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


Так как Apps Script использует движок JavaScript Rhino v1.7r3 мне придется переписать под него код приведенный в статьях. Используя следующие материалы:

es6-equivalents-in-es5

Так-же попробую привести аналоги возможностей FP без использования библиотеки Ramda



1. Первые шаги

2. Сочетаем функции

3. Частичное применение (каррирование)

4. Декларативное программирование

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

6. Неизменяемость и объекты

7. Неизменяемость и массивы

8. Линзы

9. Заключение



Функции


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


Это простая функция, написанная на 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


  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)



Заключение


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



Report Page