Functional concepts

Чистота

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

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

function half(x) {
    return x / 2;
}

Функцияhalf(x)принимает числоxи возвращает значение половиныx. Если мы передадим этой функции аргумент 8, она вернет 4. После вызова чистая функция всегда может быть заменена результатом своей работы. Например, мы могли бы заменитьhalf(8)на 4: где бы эта функция не использовалась в нашем коде, подмена никак не повлияла бы на конечный результат. Это называется ссылочной прозрачностью.

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

var someNum = 8;

// это НЕ чистая функция
function impureHalf() {
  return someNum / 2;
}

Итого:

  • Чистые функции должны принимать аргументы.

  • Одни и те же входные данные (аргументы) всегда произведут одинаковые выходные данные (вернут одинаковый результат).

  • Чистые функции основываются только на внутреннем состоянии и не изменяют внешнее (примечание:console.logизменяет глобальное состояние).

  • Чистые функции не производят побочных эффектов.

  • Чистые функции не могут вызывать нечистые функции.

Нечистые функции

Нечистая функция изменяет состояние вне своей области видимости. Любые функции с побочными эффектами — нечистые, ровно как и процедурные функции без возвращаемого значения. Рассмотрим следующие примеры:

// нечистая функция производит побочный эффект
function showAlert() {
  alert('This is a side effect!');
}
// нечистая функция изменяет внешнее состояние
var globalVal = 1;
function incrementGlobalVal(x) {
  globalVal += x;
}
// нечистая функция процедурно вызывает чистую фунцию
function proceduralFn() {
  const result1 = pureFnFirst(1);
  const result2 = pureFnLast(2);
  console.log(`Done with ${result1} and ${result2}!`);
}
// нечистая функция выглядит чистой,
// но возвращает разные значения
// при одинаковых входных данных
function getRandomRange(min, max) {
  return Math.random() * (max - min) + min;
}

Побочные эффекты в JavaScript

Когда функция или выражение изменяет состояние вне своего контекста, результат является побочным эффектом. Примеры побочных эффектов: вызов API, манипулирование DOM, вывод alert, запись в базу данных и так далее. Если функция производит побочные эффекты, она считается нечистой. Функции, вызывающие побочные эффекты, менее предсказуемы и их труднее тестировать, поскольку они приводят к изменениям вне их локальной области видимости.

Вывод

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

Состояние

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

С состоянием

Программы, приложения или компоненты с состоянием хранят в памяти данные о текущем состоянии. Они могут изменять состояние, а также имеют доступ к его истории. Следующий пример демонстрирует это:

var number = 1;
function increment() {
  return number++;
}
// глобальная переменная изменяется: number = 2
increment();

Без состояния

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

var number = 1;
function increment(n) {
  return n + 1;
}
// глобальная переменная НЕ изменяется: возвращает 2
increment(number);

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

Вывод

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

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

Концепции неизменяемости и изменяемости более туманны в JavaScript, чем в некоторых других языках программирования. Тем не менее, вы много услышите о неизменяемости при чтении о функциональном программировании в JS. Важно знать, что эти термины означают в классическом понимании и как они реализуются в JavaScript. Определения достаточно просты:

Неизменяемый

Если объект является неизменяемым, его значение не может быть изменено после создания.

Изменяемый

Если объект изменяем, его значение может быть изменено после создания.

Реализация: неизменяемость и изменяемость в JavaScript

В JavaScript строки и числовые литералы реализованы неизменяемыми. Это легко понять, если рассмотреть, как мы работаем с ними:

var str = 'Hello!';
var anotherStr = str.substring(2);
// результат: str = 'Hello!' (не изменена)
// результат: anotherStr = 'llo!' (новая строка)

Используя метод .substring()на нашемHello!, строка не изменяет исходную строку. Вместо этого она создает новую строку. Мы могли бы переопределить значение переменнойstrна что-то другое, но, как только мы создали нашу строкуHello!, она навсегда останетсяHello!

Числовые литералы также неизменяемы. Следующий пример всегда будет иметь одинаковый результат:

var three = 1 + 2;
// результат: three = 3

Ни при каких обстоятельствах 1 + 2не может стать чем-либо, кроме 3.

Это демонстрирует, что в JavaScript присутствует реализация неизменяемости. Однако разработчики JS знают, что язык позволяет изменить многое. Например, объекты и массивы изменяемы. Рассмотрим следующий пример:

var arr = [1, 2, 3];
arr.push(4);
// результат: arr = [1, 2, 3, 4]

var obj = { greeting: 'Hello' };
obj.name = 'Jon';
// результат: obj = { greeting: 'Hello', name: 'Jon' }

В этих примерах исходные объекты изменены. Новые объекты не возвращаются.

Примеры

Функциональное программирование в JavaScript хорошо развивается. Но по своей сущности JS — очень изменчивый язык, состоящий из множества парадигм. Ключевая особенность функционального программирования — неизменяемость. Другие функциональные языки выбросят ошибку, когда разработчик попытается изменить неизменяемый объект. Тогда как мы можем примирить врожденную изменяемость JS при написании функционального или функционального реактивного JS?

Когда мы говорим о функциональном программировании в JS, слово «неизменяемое» используется много, но разработчик обязан всегда держать ее в голове. Например, Redux полагается на одно неизменяемое дерево состояний. Однако сам JavaScript способен изменять объект состояния. Чтобы реализовать неизменяемое дерево состояний, нам нужно каждый раз при изменении состояния возвращать новыйобъект состояния.

Для неизменяемости объекты JavaScript также могут быть заморожены с помощьюObject.freeze(obj). Обратите внимание, что это "неглубокая" заморозка: значения объектов внутри замороженного объекта все еще могут быть изменены. Для гарантированной неизменяемости такие функции "глубокой" заморозки, как Mozilla deepFreeze() и npm deep-freeze могут рекурсивно замораживать объекты. Замораживание наиболее применимо в тестах, а не в приложении. Тесты будут оповещать разработчиков о возникновении изменений, чтобы их можно было исправить и избежать загромождающегоObject.freezeв основном коде.

Существуют также библиотеки, поддерживающие неизменяемость в JS. Mori предоставляет постоянные структуры данных на основе Clojure. Immutable.js от Facebook также предоставляет неизменяемые коллекции для JS. Библиотеки утилит, такие как Underscore.js и lodash, предоставляют методы и модули для более функционального стиля программирования (стало быть, направленного на неизменяемость).

Вывод

В целом, JavaScript — язык с сильной изменяемостью. Некоторые стили JS-кодирования опираются на эту врожденную изменяемость. Однако, при написании функционального JS, реализация неизменяемости требует внимательности. Если вы что-то нечаянно модифицируете, JS не будет выбрасывать ошибки. Тестирование и библиотеки могут помочь, но работа с неизменяемостью в JS требует практики и методологии.

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

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

Last updated