Способ писать более безопасный код и ограничивать побочные эффекты.

Кто еще любит писать функции без побочных эффектов?

Я думаю, что мы, как программисты, все делаем.

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

Давай займемся этим.

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

Когда дело доходит до чистых функций, вам нужно помнить только две вещи:

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

Возврат того же результата при тех же аргументах

Давайте посмотрим на простой пример ниже:

const DISCOUNT = 0.5;
const calculatePrice = price => price * DISCOUNT;
let actualPrice = calculatePrice(15); // 7.5

Является ли функция calculatePrice чистой? Нет, это не так.

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

Чтобы сделать его чистой функцией, вам нужно добавить к функции еще один параметр, например:

const DISCOUNT = 0.5;
const calculatePrice = (price, discount) => price * discount;
let actualPrice = calculatePrice(15, DISCOUNT); // 7.5

Как видите, он удовлетворяет первому правилу чистой функции.

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

const saySomething = threshold => {
  if (Math.random() > threshold) {
    return ‘I am something’;
  } else {
    return ‘I am something else’;
  }
}
saySomething(0.5);

Не вызывает побочных эффектов

Что такое побочный эффект?

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

let totalPrice = 15;
const calculateFinalPrice = discount => {
  if (totalPrice > 10) {
    totalPrice = totalPrice * discount;
  }
  
  return totalPrice;
}
console.log(calculateFinalPrice(0.2)); // 3
console.log(totalPrice); // 3

В приведенном выше примере переменная totalPrice удовлетворяет условию и изменяется на новое значение.

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

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

let totalPrice = 15;
const calculateFinalPrice = discount => {
  let finalPrice = totalPrice;
  
  if (finalPrice > 10) {
    finalPrice = finalPrice * discount;
  }
 
  return finalPrice;
}
console.log(calculateFinalPrice(0.2)); // 3
console.log(totalPrice); // 15

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

2. Неизменность

Вернувшись к приведенному выше примеру, вы увидите, что я не изменял непосредственно переменную totalPrice, я получил новую переменную и внес в нее некоторые изменения.

Это концепция неизменности: объект должен оставаться неизменным с течением времени, или мы должны сделать его неизменным. Таким образом, ваш код будет более безопасным и управляемым. В общем, вы можете избежать неожиданного поведения, сделав вещи неизменными.

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

Возьмем, к примеру, примитивные типы:

let totalPrice = 13;
let finalPrice = totalPrice;
finalPrice = finalPrice * 0.3;
console.log(totalPrice); // 13
console.log(finalPrice); // 3.9

Когда вы присваиваете значение примитивной переменной, назначается фактическое значение. Вот почему в приведенном выше примере изменение finalPrice не влияет на значение totalPrice, хотя мы присваиваем totalPrice значению finalPrice в первую очередь.

Объекты и массивы действуют иначе:

let languages = [‘JavaScript’, ‘C++’, ‘Kotlin’, ‘Golang’];
let clonedLanguages = languages;
clonedLanguages[0] = ‘Java’;
console.log(clonedLanguages); // [“Java”, “C++”, “Kotlin”, “Golang”]
console.log(languages); // [“Java”, “C++”, “Kotlin”, “Golang”]

Когда вы назначаете объект или массив другому, вы назначаете ссылку на память, которая содержит фактическое значение. Вот почему в приведенном выше примере изменение значения clonedLanguages ​​ приводит к одинаковому изменению языков, поскольку оба они указывают на один и тот же адрес в памяти.

Поначалу это может немного раздражать, когда вы приближаетесь к неизменяемости. Да, вам нужно больше кодировать, создавая новые экземпляры в процессе. Однако в конечном итоге это будет вас раздражать, потому что это поможет избежать неожиданного поведения. Если вы используете redux для своих проектов на основе React, вы меня понимаете.

3. Ссылочная прозрачность

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

Например:

const sumOf = (a, b) => a + b;
sumOf(2, 3); // 5

Как видно из примера, он прозрачен по ссылкам, потому что при любых заданных a и b результат sumOf (a, b) остается такой же.

Когда это не прозрачно по ссылкам? Взгляните на простое изменение в приведенном выше примере.

const sumOf = (a, b) => {
  console.log(a, b);
  return a + b;
}
const take1 = (a, b) => {
  let result = sumOf(a, b);
  return result + result;
}
const take2 = (a, b) => {
  return sumOf(a, b) + sumOf(a, b);
}
take1(1, 2);
take2(1, 2);

Вы можете подумать, что результаты take1 (1, 2) и take2 (1, 2) одинаковы, это 6. Однако это не так. take1 печатает журнал только один раз, а take2 - дважды. Вот реальный результат:

take1(1, 2); // 1 2 result = 6
take2(1, 2); // 1 2 1 2 result = 6

Почему вы должны заботиться о ссылочной прозрачности? Потому что это делает ваш код тестируемым и предсказуемым, поэтому вы можете избежать какой-либо неопределенности при написании каждой отдельной строки кода.

4. Первоклассные функции

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

Поскольку мы используем функции как значения, мы можем хранить их в переменных, объектах и ​​массивах.

Например:

Назначение функций переменным:

const sum = (a, b) => a + b;

Передача функций в качестве параметров:

const averageOfTwo = (sum, a, b) => sum(a, b) / 2;

Возврат функции в результате:

const operation = (a, b) => {
  return (a, b) => a + b;
}
const op = operation(3, 5);
console.log(op(5, 3)); // 8

5. Функции высшего порядка

Вы знаете, что функция является функцией высшего порядка, если:

  • В качестве аргументов он принимает другие функции.
  • Или он возвращает функцию в качестве своего результата.

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

Некоторые общие функции высшего порядка в JavaScript, которые вы, скорее всего, использовали, но до сих пор не знали об их концепции:

.карта

const names = [‘Amy’, ‘James’, ‘Tom’];
const uppercasedNames = names.map(name => name.toUpperCase());
console.log(uppercasedNames); // [“AMY”, “JAMES”, “TOM”]

.для каждого

const names = [‘Amy’, ‘James’, ‘Tom’];
names.forEach(name => console.log(name));

.фильтр

const numbers = [3, 4, 1, 6, 7, 10];
const evenNumbers = numbers.filter(number => number % 2 === 0);
console.log(evenNumbers); // [4, 6, 10]

Заключение

Функциональное программирование - отличный стандарт, которому вы должны следовать, чтобы улучшить качество и улучшить ремонтопригодность вашего кода.

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

Надеюсь, эта история окажется для вас полезной.

Дальнейшее чтение