Чистота функции определяется двумя условиями:
1. Функция детерминирована
2. Функция не имеет побочных эффектов
Если оба этих условия истинны, то тогда функцию можно считать чистой.
Детерминированность функции означает, что любому набору входных данных можно однозначно сопоставить результат и этот результат будет всегда таким для данного набора аргументов, независимо ни от чего. То есть умножение, сложение, нахождение максимума - это все детерминированные функции, а вот функции вычисляющие псевдослучайное число, текущую дату или температуру с датчика - нет, так как зависят от внешних изменяющихся условий.
Побочные эффекты - это нечто, что меняет окружающую среду, например запись на диск или в чужую (не аллоцированную самой функцией) память, сетевой запрос, запуск процесса или потока, отправка сообщения другому потоку, синхронизация потоков и т.д.
Из этого становится верно утверждение, что если вызов чистой функции заменить на ее результат, то поведение программы от этого не изменится. А от сюда напрашивается еще один вывод, что программа состоящая только из чистых функций бесполезна. Но можно вынести все грязные операции на край приложения, а бизнес логику описать в виде чистых функций, что даст ей все преимущества таких функций, вроде предсказуемости и простой тестируемости.
Что же качается асинхронности, то большинство асинхронных операций (если не все) порождают побочные эффекты. Но можно выносить их на край приложения, а основную логику строить в виде чистых функций. Покажу на примере такого сценария:
1. Пользователь кликает на кнопку
2. Данные из стейта подготавливаются и отправляется запрос на сервер с ними
3. Когда пришел ответ данные снова преобразуются и обновляется стейт
let state = {
// для простоты будем считать что эти данные уже есть
formData: {
text1: 'Hello',
text2: 'world',
checkbox1: true,
checkbox2: false
},
// а сюда мы должны положить результат запроса
requestResult: null
};
// querySelector не детерминирован, а подписка на событие - побочный эффект
document.querySelector('.button').addEventListener('click', clickHandler);
// prepareFormData - чистая функция
function prepareFormData(formData) {
return {
text: `${formData.text1} ${formData.text2}`,
checkboxes: [
['checkbox1', formData.checkbox1],
['checkbox2', formData.checkbox2]
].filter(([, v]) => v).map(([v]) => v)
};
}
const API_URL = '/api/data';
// createRequestSender - чистая функция
// API_URL - константа, она не меняет результат
// сама createRequestSender не делает запроса к api
// она просто "вычисляет" из preparedFormData функцию, притом детерминировано
function createRequestSender(preparedFormData) {
const body = JSON.stringify(preparedFormData);
// а вот возвращаемая функция уже грязная:
// хотя она и вполне детерминирована,
// но она вызывает fetch и response.json имеющие побочные эффекты
// но это никак не влияет на чистоту внешней функции createRequestSender, так как та ее не вызывает
return async () => {
const response = await fetch(API_URL, {
method: 'POST',
body
});
const result = await response.json();
return result;
};
}
// mapResponseToState - чистая функция,
// а вот если бы мы напрямую изменили state вместо копирования его в результат
// это бы был побочный эффект, так как state не принадлежит mapResponseToState
function mapResponseToState(state, responseResult) {
return {
...state,
requestResult: (responseResult.ok
? {
showSuccess: true,
showError: false,
text: responseResult.result
}
: {
showSuccess: false,
showError: true,
text: responseResult.error
}
)
};
}
// обработчик клика clickHandler будет тем самым грязным краем
async function clickHandler() {
// чистая операция
const requestSender = createRequestSender(prepareFormData(state.formData));
// побочный эффект
const result = await requestSender();
// еще чистая операция
const newState = mapResponseToState(state, result);
// и снова побочный эффект
state = newState;
}