​Дещо про мутації/стан

Ніщо не є хорошим чи поганим само по собі і мутації (зміни стану/значення, на яке посилається змінна) не є вийнятком.

Через поверхневе розуміння або його відсутність склалось так, що до мутацій в коді, особливо в останні декілька років і серед «функціональщиків»-початківців існує погане ставлення. В цій публікації я розкажу що саме погано, а що дуже добре в мутаціях і як використовувати лише добре.

Погане

Погане у коді – це не самі мутації, а можлива непередбачуваність, що виникає у конкурентних та асинхронних архітектурах, коли деяке значення використовується у декількох функціях, що змінюють його в невизначеній/непередбачуваній послідовності. Тобто, проблема у тому, що f(g(x)) != g(f(x)), а ми, через асинхронність/конкурентність не маємо чіткого порядку застосунку f() та g().

Проблема навіть не у чіткому порядку, а в тому, що він важливий та відсутності атомарності операцій. Наприклад, немає нічого поганого у тому, що ми запишемо в журнал, спочатку запис А, а потім запис Б, або навпаки. Проблема у тому, що якщо записи А та Б не атомарні, то ми отримаємо не непередбачуваний порядок записів А та Б (що нас влаштовує), а змішану, зруйновану інформацію із обох записів. 

Хороше

Код з мутаціями майже завжди буде значно ефективнішим за код без мутацій. Наприклад, імперативні/мутабельні вектор чи масив будуть у більшості випадків ефективнішими за функціональний/імутабельний список.

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

Безпечні мутації

Існує всього два різновиди підходів до безпечного використання мутацій:

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

Другий підхід – умовно об‘єктно-орієнтований, хоча так об‘єкти майже ніде не реалізовані. Його суть полягає у тому, що замість блокування за допомогою семафорів/мютексів, ми надсилаємо об’єкту, що інкапсулює певні пов’язані між собою змінні, мутуючі команди-повідомлення, що додаються у чергу та послідовно опрацьовуються об‘єктом.

В такий спосіб значно простіше (потрібно менше уваги розробників та менше допоміжного коду) вирішується проблема атомарності операцій і без блокувань. Саме в цьому я бачу найбільшу цінність т.з. ООП (відправки повідомлень).

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

IdentityMap та розподілені об’єктні системи

Аби гарантувати, що є лише один об‘єкт, що змінює стан чогось, використовується т.з. паттерн Identity Map. Застосування цього паттерну гарантує, що сутність (те, що існує в часі і має стан) буде відображена лише одним об‘єктом/чергою. В іншому випадку користь застосування об‘єктів/черг буде зведена нанівець.

Якщо ми працюємо з розподіленою системою, то і реалізація identity map має бути розподіленою, аби гарантувати, що будь-яка сутність представлена лише одним єдиним об’єктом/чергою повідомлень.

P.S.

Актори Erlang та goroutines + channels в Golang зводять т.з. ОО підхід до самої його суті. Так вже повелося, що не ООП мови краще реалізують ідеї ООП, аніж т.з. ООП-мови.

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *