Курс СРС SRS12
до ЛР6 рівень 3 → «5»↔ Лабораторна 6↔ Лекція 6⌨️ команди 3
Самостійна робота · ~3 год

Патерн «зробити некоректний стан непредставним» (make illegal states unrepresentable)

🌌 Архітектура без дір — С.І.Д. із гордістю майстра закреслює «дірку» в структурі вузла — комбінацію на кшталт «заблокований, але без коду блокування», яку недбалий тип пропустив би мовчки. Дивись, каже він, як гарно це лагодиться: правильно дібраний enum робить таку дірку фізично неможливою, а не просто малоймовірною.

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

Коли структура даних дозволяє більше, ніж домен реально має

Типова початківська помилка проєктування — описувати стан через набір незалежних полів Option/bool, коли насправді ці поля жорстко взаємопов'язані. Розгляньмо struct Node { is_compromised: bool, compromised_by: Option<String>, compromised_at: Option<u64> }. Формально ця структура допускає чотири комбінації полів is_compromised і наявності compromised_by: скомпрометований вузол без вказаного нападника, нескомпрометований вузол із заповненим compromised_by, розбіжність між compromised_by і compromised_at (одне є, інше None) — жодна з цих комбінацій не має сенсу в предметній області, але тип-система нічим не заважає їх створити. Кожне місце коду, що читає ці поля, змушене або довіряти («такого не буває на практиці»), або писати захисні перевірки рантайму на комбінації, які взагалі не повинні існувати.

Принцип «зроби некоректний стан непредставним» (make illegal states unrepresentable), сформульований у спільноті функціонального програмування (Yaron Minsky) і природно застосовний у Rust завдяки алгебраїчним типам даних, пропонує інший підхід: замість набору незалежних полів — один enum, кожен варіант якого несе рівно ті дані, які осмислені саме для цього стану. enum NodeState { Clean, Compromised { by: String, at: u64 } }. Тепер компілятор фізично не дозволить створити «скомпрометований, але без нападника» — такого варіанта просто не існує в типі. Некоректний стан не забороняється перевіркою — він не має способу бути записаним.

Цей підхід систематично зменшує кількість if/assert у кодовій базі, призначених для перевірки «внутрішньоузгодженості» даних: якщо тип структурований правильно, така перевірка стає зайвою — вона гарантована конструкцією типу, а не логікою функції, що з ним працює. Це і зменшує кількість місць, де можна помилитися (немовби забути одну з перевірок у якомусь віддаленому куточку коду), і робить кожен match над таким enum самодокументованим описом усіх реально можливих станів системи.

Практичний прийом виявлення таких «дір» у наявному коді — шукати поля, які завжди заповнені/порожні разом (кореляція Option-полів), і булеві прапорці, що фактично дублюють інформацію, яку вже несе інше поле (наприклад, окремий is_compromised: bool поряд з Option<String> компрометатора — сам факт Some/None уже й є цим прапорцем, тож зберігати обидва — джерело розбіжності). Рефакторинг зазвичай іде за схемою: знайти групу корельованих Option/bool-полів → визначити список реально можливих станів (не комбінацій полів!) предметної області → створити один enum з відповідними варіантами, кожен із власним набором релевантних даних → замінити всі місця читання старих полів на match над новим enum (компілятор вкаже кожне таке місце, якщо старі поля прибрати одразу).

Варто зауважити межу застосовності: цей принцип найкраще працює, коли стани справді взаємовиключні (вузол не може бути одночасно «чистим» і «скомпрометованим»). Якщо ж атрибути предметної області справді незалежні один від одного (наприклад, is_online: bool і firmware_version: String не корелюють між собою), об'єднувати їх у штучний enum немає сенсу — комбінаторний вибух варіантів лише ускладнить код без жодної реальної переваги; принцип застосовується саме до полів, чия залежність одне від одного і є джерелом потенційно некоректного стану.

📖 ОПРАЦЮВАТИ

Переглянути класичну статтю/розділ спільноти Rust про «making illegal states unrepresentable» і переробити приклад struct із необов'язковими (Option) полями на enum із варіантами, що несуть лише релевантні для кожного стану дані.

🖥️ ТЕРМІНАЛ — СПРОБУЙ САМ
🎯 ЗАВДАННЯ
  1. Написати struct Node із двома окремими полями is_compromised: bool та compromised_by: Option, навмисно створити екземпляр із логічно суперечливою комбінацією (наприклад, is_compromised: false, compromised_by: Some(...)), і показати, що компілятор це дозволяє.
  2. Переробити цю саму структуру на enum NodeState { Clean, Compromised { by: String, at: u64 } } і показати, що суперечлива комбінація даних більше фізично не може бути виражена жодним значенням типу.
  3. Знайти (або сконструювати) ще один приклад із двома-трьома корельованими Option-полями в описі гіпотетичної предметної області (наприклад, статус завантаження файлу: progress: Option, error: Option, result: Option<Vec>) і переробити його на enum зі станами Idle/InProgress/Failed/Done.
  4. Пройтися по всіх місцях коду з попереднього завдання, де читалися старі поля struct, замінити їх на match над новим enum і переконатися, що компілятор вимагає обробки кожного стану.
  5. Навести й письмово обґрунтувати контрприклад — два незалежні поля (не стани одне одного), які НЕ варто об'єднувати в enum, пояснивши, чому в цьому випадку принцип не застосовний.
❓ КОНТРОЛЬНІ ПИТАННЯ
  1. Наведіть приклад struct із двома bool/Option полями, комбінація яких описує логічно неможливий стан, і покажіть, як enum усуває саму можливість його створення.
  2. У чому принципова відмінність між «забороною» некоректного стану перевіркою в рантаймі і тим, щоб зробити його «непредставним» типом?
  3. Як принцип make illegal states unrepresentable зменшує кількість захисних if/assert у кодовій базі?
  4. Які ознаки в наявному struct вказують на те, що набір полів варто переробити на enum?
  5. Що робить кожен варіант такого enum самодокументованим порівняно з набором окремих полів?
  6. У якому випадку об'єднання двох полів в один enum не є доцільним, навіть якщо технічно можливе?
  7. Що станеться з рештою кодової бази, якщо після переходу на enum видалити старі поля struct, на які код досі посилався?
✅ САМОПЕРЕВІРКА

Наведіть приклад struct із двома bool/Option полями, комбінація яких створює логічно неможливий стан, і покажіть, як enum усуває саму можливість такої комбінації.

🥚 ПАСХАЛКА
У перехопленому журналі «ТехНови» є вузол зі станом is_compromised: false, compromised_by: Some("C1D") — та сама суперечлива комбінація зі структурного прикладу, тільки справжня. С.І.Д. запевняє, що це не він — і не кіт. Після переходу на enum NodeState такий запис стане непредставним, і тому, хто лишає підписи на мінус третьому, доведеться придумати новий спосіб.
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 3

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

cargo run
Збирає й одразу запускає програму; аргументи після -- йдуть програмі (cargo run -- staff.log).
cargo
Система збірки й менеджер пакетів Rust: створює проєкт, тягне залежності (крейти), збирає, тестує, запускає.
cargo build
Компілює проєкт у target/debug/; --release — з оптимізаціями у target/release/.

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.

← SRS11усі темиSRS13 →