Повернутися до блогу
20 лист. 2024 р.
7 хвилин читання

Чисті функції, мутації та побічні ефекти

Пояснення і приклади використання

У контексті JS, React, Redux та функціонального програмування часто використовуються такі терміни як

Давайте з’ясуємо, що вони означають, як використовуються (в контексті JS), та, нарешті, поставимо крапку в термінологічній плитунині, яка притаманна цим термінам.

Мутації

ninja-turtles

Мутація в програмуванні означає зміну стану об’єкта, змінної чи структури даних після їх створення. І нажаль ніяк не пов’язана з черепашками-ніндзя, або з людьми-мутантами.

У JavaScript примітивні значення (всі типи данних крім обʼєктів) є іммутабельними (immutable), тобто вони не можуть бути змінені, але втім, можуть бути переназначені.

Часто можна почути, що let є мутабельним, а const — ні. Це не зовсім правильно.

Різниця між let та const саме в тому, що const не можна переназначити, але так само як і для let, обʼекти і массиви в const не є іммутабельними.

Приклад

const person = {name: 'Cassius Marcellus Clay'};
person.name = 'Muhammad Ali';

У цьому прикладі властивість name об’єкта person змінюється. Це є мутацією.

Чому це вважається поганим?

Мутації самі по собі не є шкідливими, але вони можуть призводити до помилок і ускладнювати розуміння коду.

Короткий приклад:

const a = [1, 2, 3];
const b = a;
b.push(4);
console.log(a); // [1, 2, 3, 4]

У цьому прикладі ми мутуємо масив b, але він вказує на той самий об’єкт, що і a. Це може призвести до непередбачуваної поведінки.

Щоб уникнути цього, можна використовувати методи, які повертають новий об’єкт, а не мутують старий:

const a = [1, 2, 3];
const b = {...a, 4};
console.log(a); // [1, 2, 3]

Іммутабельність в React/Redux

React

Одне з основних правил React - це те що props і state повинні бути іммутабельними.

Redux

Так само і в Redux - перше правило в Redux Style Guide - не мутувати стан.

Бібліотеки для роботи з іммутабельністю

Для того щоб уникнути мутацій, можна використовувати вбудований в JavaScript метод Object.freeze або бібліотеки, які допомагають працювати з іммутабельністю, наприклад:

Побічні ефекти (Side Effects)

side-effects

Побічний ефект — це будь-яка зміна в системі, яка впилває на зовнішній світ.

Можливо, зараз це звучить дещо абстрактно.

Коли ви викликаєте функцію, ваш комп’ютер працює інтенсивніше, виділяючи тепло. Це тепло може поступово підвищувати температуру в кімнаті. Чи можна це вважати побічним ефектом функції?

Що ж, це залежить від контексту:

В контексті фізики — так. Але в контексті программи, яку ми розробляємо, — ні. Зазвичай побічні ефекти в програмуванні стосуються лише того, що відбувається в межах программи, наприклад:

  • Зміна глобальної змінної.
let counter = 0;

function increment() {
	counter++; // Побічний ефект: змінюється зовнішній стан
}

increment();
console.log(counter); // 1
  • Логування в консоль.
function logMessage(message) {
	console.log(message); // Побічний ефект: вивід у консоль
}

logMessage('Hello, world!');

Логування не змінює стан програми, але впливає на середовище виконання, залишаючи слід у консолі.

  • Зміни в DOM.
function updateTitle(newTitle) {
	document.title = newTitle; // Побічний ефект: зміна заголовка сторінки
}

updateTitle('Новий заголовок');
console.log(document.title); // "Новий заголовок"

Побічні ефекти в React/Redux

React

В функціональних компонентах React місцем для побічних ефектів є хук useEffect. Там можуть відбуватися такі побічні ефекти як:

  • Запити до сервера
useEffect(() => {
	fetch('https://api.example.com/data')
		.then((response) => {
			if (!response.ok) {
				throw new Error('Network response was not ok');
			}
			return response.json();
		})
		.then((data) => setData(data))
		.catch((error) => console.error('Fetch error:', error));
}, []);

Варто зазначити, що для запитів до сервера можна використовувати бібліотеки, які дозволяють писати код більш декларативно, наприклад React Query або SWR.

  • Підписка на події

    useEffect(() => {
    	const handleOffline = () => setIsOffline(true);
    	const handleOnline = () => setIsOffline(false);
    
    	window.addEventListener('offline', handleOffline);
    	window.addEventListener('online', handleOnline);
    
    	return () => {
    		window.removeEventListener('offline', handleOffline);
    		window.removeEventListener('online', handleOnline);
    	};
    }, []);
    
  • Робота з локальним сховищем

    useEffect(() => {
    	const data = JSON.parse(localStorage.getItem('data'));
    	setData(data);
    }, []);
    
  • Маніпуляції з DOM

    useEffect(() => {
    	document.title = 'Новий заголовок';
    }, []);
    

Redux

В Redux управління побічними ефектами у ньому реалізується через middleware. Найпопулярнішим middleware для цього — Redux Thunk

const fetchUserById = createAsyncThunk('users/fetchByIdStatus', async (userId: number, thunkAPI) => {
	const response = await userAPI.fetchById(userId);
	return response.data;
});

Чисті функції (Pure Functions)

Pure function

Чисті функції — це функції, які мають дві властивості:

  1. Вони не мають побічних ефектів.
  2. Детермінізм - це значить, що для одних і тих самих вхідних даних функція завжди повертає один і той самий результат.

Давайте розглянемо пару прикладі

Приклади

Чи буде ця функція чистою?

function addRandom(a) {
	return a + Math.random();
}

Для цього нам потрібно відповісти на два питання:

  1. Чи має ця функція побічні ефекти?

Ні, побічних ефектів тут немає.

  1. Чи є ця функція детермінованою? Тобто, чи завжди повертає один і той самий результат для одних і тих самих вхідних даних?

Ні, ця функція не є детермінованою, оскільки Math.random() повертає випадкове число. Через що при першому виклику addRandom(2) може повернути, наприклад, 2.123, а при другому, скажімо, — 2.456.

Отже, ця функція не є чистою.

А ця?

function add(a, b) {
	return a + b;
}

Ця функція відповідає обом вимогам чистої функції: вона не має побічних ефектів і є детермінованою.

Чисті функції в Redux

Одним з трьох основних принципов в Redux є використання чистих функцій

Ідемпотентні функції

Idempodent
Non-idempotent

Термін «ідемпотентність» означає властивість, яка проявляється в тому, що повторне застосування операції над об’єктом не змінює його стан після першого виконання.

Функція є ідемподентною, якщо її повторні виклики з однаковими вхідними даними не змінюють результат або стан після першого виклику.

Тобто вликлик функції f(x) декілька разів, наприклад f(x); f(x); f(x); дасть той самий результат, що і один виклик f(x);. (Тут варто зазначити, що в математиці ідемподентність означає, що f(f(x)) = f(x), але то вже трохи інша історія. Якщо цікаво, можете прочитати це в чудовій книзі Кайла Сімпсона Functional-Light JavaScript)

Приклади

function convertToUpper(str) {
	return str.toUpperCase();
}

Ця функція є ідемпотентною, тому що вилкик цієї функції, скажімо, 10 разів, дає той же самий еффект, що й виклик один раз. Ця функція також є й чистою. Чисті функції завжди є ідемпотентними, але не обов’язково навпаки.


function setUserActive(user) {
	user.status = 'active';
}

Ця функція є ідемпотентною, оскільки при кожному виклику з однаковим об’єктом user буде встановлювати статус active. Але ця функція не є чистою, оскільки мутує об’єкт user.


function addHiddenClass(element) {
	element.classList.add('hidden');
}

Аналогічно, функція addHiddenClass є ідемпотентною: після першого виклику клас hidden додається до елемента, і наступні виклики не змінюють стан. Однак через те, що функція мутує DOM, вона не є чистою.


Тут у нас не буде прикладів використання в React/Redux, тому що частіше термін ідемпотентність використовується в контексті API або HTTP методів.

Висновок

На цьому все. Сподіваюсь ця стаття додала трошки ясності в вищезгадані терміни. Якщо у вас виникли питання, або ви знайшли помилку, можете написати тут в коментарях або на будь-який з контактів зазначенних в футері.