Предыдущие части: Часть 1, Часть 2
Мотивация
Столько ошибок зарыто в IO, мутациях данных и посторонних эффектах существующего кода. Они появляются в разных местах по всей базе кода, начиная с принятия введенных данных, получения неожиданного ответа от http-вызова или записи в файловую систему. К сожалению, суровую реальность никто не отменял, и нам остается только смириться с этим. А так ли это?
Что бы вы сделали, если бы вам сказали, что мы можем сократить части кода, выполняющие критические и просто важные части программы? Мы могли бы сделать большую часть нашего кода чистой, а также сократить в исходной базе количество имеющегося кода, связанного с OI и побочными эффектами. Это бы упростило процесс отладки, сделав его более согласованным и понятным.
Так что же это за загадочные чистые функции? Чистая функция отвечает двум основным требованиям:
1. Она детерминирована. Это означает, что при одном и том же вводе функция всегда возвращает одинаковый результат. Если проводить параллель с математическими терминами (я быстро!), то ей соответствует четко определенная функция. Каждый ввод возвращает один и тот же вывод. Каждый раз.
Чистая функция
const add = (x, y) => x + y // Чистая функция
add – это чистая функция, потому как ее вывод зависит только от полученного аргумента. Таким образом, при одинаковых значениях будет возвращаться одинаковый результат.
Ну, а как насчет такой?
const magicLetter = '*' const createMagicPhrase = (phrase) => `${magicLetter}abra${phrase}`
В чем-то здесь подвох… Функция createMagicPhrase зависит от значения, которое является внешним по отношению к ее области видимости. Значит, она не чистая!
Нечистая функция
const fetchLoginToken = externalAPI.getUserToken
Является ли fetchLoginToken чистой функцией? Возвращает ли она каждый раз одно и то же значение? Конечно же, нет! Иногда она выполняется, иногда сервер падает, и мы видим 500-ю ошибку. А когда-то в будущем API может взять и поменяться так, что вызов станет не выполняемым. Так что раз эта функция не детерминирована, то можно с уверенностью сказать, что чистой она не является.
2. Чистая функция не вызывает побочных эффектов. Побочный эффект – это любое изменение в системе, которое заметно внешнему миру.
const calculateBill = (sumOfCart, tax) => sumOfCart * tax
Является ли calculateBill чистой функцией? Однозначно 🙂 Она отвечает двум необходимым требованиям:
· Функция зависит только от своих аргументов и возвращает значение на основании них.
· Функция не вызывает никаких побочных эффектов.
The Mostly Adequate Guide говорит нам, что побочные эффекты включают в себя (но не ограничиваются!) следующее:
· изменение файловой системы:
· добавление записи в базу данных;
· создание http-вызова;
· вызовы;
· вывод данных на экран / ведение журнала;
· получение пользовательского ввода;
· запросы к DOM;
· доступ к состоянию системы.
Для чего делать функции чистыми?
Читаемость -> С побочными эффектами наш код прочесть достаточно трудно. И так как нечистые функции не являются детерминированными, то для одного ввода они могут возвращать несколько разных значений. В итоге нам приходится писать код, который бы учитывал все возможные вероятности. Давайте посмотрим еще на один пример, теперь на базе http:
async function getUserToken(id) { const token = await getTokenFromServer(id); return token; }
В этом сниппете может возникать столько разных ошибок. Что, если id, передаваемый в getTokenFromServer, окажется некорректным? А вдруг сервер упадет и вместо ожидаемого маркера вернет ошибку? Существует огромное количество всевозможных исходов, и все их необходимо учесть, причем забыть про один из них (или несколько!) достаточно просто.
Чистая функция легче читается, т.к. для нее не требуется контекст. Все необходимые параметры она получает заранее и не коммуницирует / не вмешивается в состояние приложения.
Тестируемость -> Все чистые функции по природе своей детерминированы, поэтому писать для них модульное тестирование – сплошное удовольствие. Тут ваша функция либо работает, либо нет ?
Параллельный код -> Поскольку чистые функции зависят только от входящих значений и не вызывают побочных эффектов, то они идеально подходят для сценариев с параллельными потоками и использованием общей памяти.
Модульность и повторное использование -> Чистые функции можно воспринимать как небольшие логические единицы. Раз они зависят только от того, что получают на входе, то их можно повторно использовать в различных частях кода или проекта.
Прозрачность ссылок -> Звучит как-то очень мудрено. Когда мне впервые попалось это словосочетание, то сразу захотелось все бросить! Но в переводе на простой язык, прозрачность ссылок означает то, что вызов функции можно заменить ее выходным значением без сопутствующих корректировок поведения программы в целом. Это очень удобно при продумывании общей концепции в процессе создания чистых функций.
Все чище некуда… но разве есть в этом толк?
Важно понимать, что несмотря на все плюсы, реализовать работающую программу на одних только чистых функциях – практически невозможно. Ведь тогда созданная нами программа не будет иметь побочных эффектов, поэтому не будет выдавать внешнему миру ничего примечательного. Ну, скукотища же ???. Вместо этого мы постараемся инкапсулировать все сторонние эффекты в определенные части кода. Тогда в случае, если при написании модульного тестирования чистых и работающих (!) функций что-то сломается, то мы с легкостью найдем источник ошибки.
Акцент на чистоту
Давайте завершим нашу статью, превратив следующую нечистую функцию в чистую. Это вымышленный пример, но в нем хорошо показано, как можно провести быстрый рефакторинг нечистого кода в чистый.
let a = 4; let b = 5; let c = 6; const updateTwoVars = (a) => { b++; c = a * b; } updateTwoVars(a); console.log(b,c); // b = 6, c = 24
Для начала определим, какая именно перед нами функция. Эта функция является нечистой потому что зависит от А и В, которые являются внешними элементами по отношению к области ее видимости. Кроме того, она также мутирует (изменяет) значения переменных. Самым быстрым способом рефакторинга данной функции будет следующее:
· Сначала убедимся, что все переменные, от которых зависит функция, передаются как аргументы.
· Вместо мутации (манипуляций с) B и С мы можем вернуть новые значения и отразить их.
let a = 4; let b = 5; let c = 6; const updateTwoVars = (a, b, c) => [b++, a * b]; const updateRes = updateTwoVars(a,b,c); b = updateRes[0] c = updateRes[1]
Заключение
Мы рассмотрели доводы в пользу того, чтобы сводить код к более чистым функциям. Так наш код будет легче восприниматься, тестироваться и, что самое главное, он станет более предсказуемым. Помните, что чистые функции пишутся не для полного избавления кода от побочных эффектов, а для того, чтобы «запереть» эти эффекты в определенном месте и по максимуму их ограничить. По мере роста размеров и сложности вашей программы, данный подход еще не раз докажет вам свою состоятельность.
Перевод статьи Omer Goldberg: Javascript and Functional Programming — Pt. 3: Pure Functions