Як заборонити некоректні стани в програмі на етапі компіляції

💡 Усі статті, обговорення, новини про Python — в одному місці. Приєднуйтесь до Python спільноти!

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

Чимала кількість отриманих коментарів була про те, що «в мейнстрім-мовах це непотрібно, і там роблять не так».

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

Задача

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

Ми моделюємо це як модуль/бібліотеку/клас/... (в залежності від вибору вашої мови програмування), яку називаємо Moonbase, в API якої входять наступні функції:

  • begin_day — починає новий день (з астронавтом всередині бази, без скафандра, із закритим шлюзом. Це може бути конструктор класу або схожий механізм ініціалізації).
  • open_lock
  • close_lock
  • spacesuit_on
  • spacesuit_off
  • leave_base
  • enter_base
  • moan
  • take_sample
  • empty_pocket
  • end_day — закінчує день на базі. Це може бути деструктор класу або схожий механізм завершення.

Для простоти домовимось, що ці функції виводять повідомлення в stdout і нічого більше. З точки зору поведінки, така реалізація на Python буде вичерпною і достатньою:

 class Moonbase:
  def __init__(self):
    print ("DAY BEGINS")
  def open_lock(self):
    print ("OPENING LOCK")
  def close_lock(self):
    print ("CLOSING LOCK")
  def spacesuit_on(self):
    print ("SPACESUIT ON")
  def spacesuit_off(self):
    print ("SPACESUIT OFF")
  def leave_base(self):
    print ("LEAVING BASE")
  def enter_base(self):
    print ("ENTERING BASE")
  def moan(self):
    print (". o O (Ugh! Damn moon! I am too old for this!)")  
  def take_sample(self):
    print ("MINING REGOLITH")
  def empty_pocket(self):
    print ("EMPTYING POCKET")
  def end_day(self):
    print ("DAY ENDS")

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

Додаткові правила

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

  1. Скафандр можна одягнути тільки якщо він не одягнутий.
  2. Скафандр можна зняти тільки якщо він одягнутий.
  3. Шлюз можна відкрити тільки якщо він закритий.
  4. Шлюз можна закрити тільки якщо він відкритий.
  5. Щоб відкрити шлюз, скафандр має бути одягнутий.
  6. Кишені скафандра мають бути порожніми, щоб його можна було одягнути (інакше крихти реголіту можуть пошкодити скафандр).
  7. Скафандр не можна знімати ззовні.
  8. Скафандр не можна знімати, якщо шлюз відкритий.
  9. Скафандр не можна знімати, якщо його кишені повні (інакше крихти реголіту можуть пошкодити скафандр).
  10. Базу можна залишити тільки якщо астронавт всередині, у скафандрі, і шлюз відкритий.
  11. Якщо ви вийшли назовні, ви обов’язково повинні взяти зразок реголіту. Тобто ви можете увійти на базу тільки з повною кишенею скафандра і відкритим шлюзом.
  12. Астронавт не повинен нарікати на життя на Місяці у скафандрі — це буде записано і погано вплине на його кар’єру.
  13. Збір зразків реголіту можна здійснювати тільки ззовні. Це створює багато пилу, тому це потрібно робити тільки з закритим шлюзом. Кишеня скафандра має бути порожньою.
  14. Кишеню скафандра можна очистити будь-коли. Якщо це зроблено ззовні, це означає, що астронавт більше не має зразка і, можливо, йому доведеться отримати новий для повернення на базу.
  15. Наприкінці дня астронавт повинен бути всередині бази, без скафандра, із закритим шлюзом.

Ми можемо додати стан до наївної реалізації Python вище і використовувати його для забезпечення цих правил (але лише на етапі виконання, тож це лише ілюстрація, а не бажане рішення):

class Moonbase:
  lockOpen = False
  suitOn  = False
  isOutside = False
  pocketsFull = False
  
  def __init__(self):
    print ("DAY BEGINS")
  def open_lock(self):
    assert(not self.lockOpen)
    assert(self.suitOn)
    print ("OPENING LOCK")
    self.lockOpen=True
  def close_lock(self):
    assert(self.lockOpen)
    print ("CLOSING LOCK")
    self.lockOpen=False
  def spacesuit_on(self):
    assert(not self.suitOn)
    assert(not self.pocketsFull)
    print ("SPACESUIT ON")
    self.suitOn=True
  def spacesuit_off(self):
    assert(self.suitOn)
    assert(not self.isOutside)
    assert(not self.lockOpen)
    assert(not self.pocketsFull)
    print ("SPACESUIT OFF")
    self.suitOn=False
  def leave_base(self):
    assert(self.lockOpen)
    assert(self.suitOn)
    assert(not self.isOutside)
    print ("LEAVING BASE")
    self.isOutside=True
  def enter_base(self):
    assert(self.isOutside)
    assert(self.pocketsFull)
    assert(self.lockOpen)
    print ("ENTERING BASE")
    self.isOutside=False
  def moan(self):
    assert(not self.suitOn)
    print (". o O (Ugh! Damn moon! I am too old for this!)")  
  def take_sample(self):
    assert(self.isOutside)
    assert(not self.lockOpen)
    assert(not self.pocketsFull)
    print ("TAKING REGOLITH SAMPLE")
    self.pocketsFull=True
  def empty_pockets(self):
    assert(self.pocketsFull)
    print ("EMPTYING POCKETS")
    self.pocketsFull=False
  def end_day(self):
    assert(not self.isOutside)
    assert(not self.lockOpen)
    assert(not self.suitOn)
    assert(not self.pocketsFull)
    print ("DAY ENDS")

Відкрите питання

Чи можемо ми забезпечити виконання всіх цих правил на етапі компіляції — так, щоб код, який порушує їх, взагалі не можна було б скомпілювати та запустити? Наприклад, щоб отака програма (псевдокод) не компілювалась:

  begin_day ()
  spacesuit_on
  open_lock
  leave_base
  close_lock
  /* Cannot take spacesuit off, will suffocate, so we should get compile error on the next line */
  spacesuit_off
  end_day

Я зробив маленький проєкт на GitHub, який містить реалізації на Haskell, та на OCaml (інтерфейс бібліотеки, та його імплементація). В них обох коректність переходів контролюється за рахунок кодування поточного стану системи лише в сигнатурах функцій. Що лишає реалізацію API дуже простою, майже як в моєму першому прикладі на Python (який не перевіряє ніяких правил взагалі). Для Haskell код модуля Moonbase займає 53 (непустих) рядки, для OCaml — 24. Ось, наприклад, імплементація Moonbase на OCaml:

type ('lock, 'suit, 'location, 'pockets) state = State
let begin_day () = print_endline "DAY BEGINS"; State
let open_lock _ = print_endline "OPENING LOCK"; State
let close_lock _ = print_endline "CLOSING LOCK"; State
let spacesuit_on _ = print_endline "SPACESUIT ON"; State
let spacesuit_off _ = print_endline "SPACESUIT OFF"; State
let leave_base _ = print_endline "LEAVING BASE"; State
let enter_base _ = print_endline "ENTERING BASE"; State
let moan _ = print_endline ". o O (Ugh! Damn moon! I am too old for this!)"; State  
let take_sample _ = print_endline "TAKING REGOLITH SAMPLE"; State
let empty_pockets _ = print_endline "EMPTYING POCKETS"; State
let end_day _ = print_endline "DAY ENDS"

І ось сігнатури, які, власне, і забезпечують дотримання правил:

type ('lock, 'suit, 'location, 'pockets) state
 
val begin_day     : unit -> ([`lock_closed], [`spacesuit_off], [`inside], [`empty]) state

val open_lock     : ([`lock_closed], [`spacesuit_on], 'location, 'pockets) state 
                  -> ([`lock_open], [`spacesuit_on], 'location, 'pockets) state

val close_lock    : ([`lock_open], [`spacesuit_on], 'location, 'pockets) state 
                  -> ([`lock_closed], [`spacesuit_on], 'location, 'pockets) state

val spacesuit_on  : ([`lock_closed], [`spacesuit_off], [`inside], [`empty]) state 
                  -> ([`lock_closed], [`spacesuit_on], [`inside], [`empty]) state

val spacesuit_off : ([`lock_closed], [`spacesuit_on], [`inside], [`empty]) state
                  -> ([`lock_closed], [`spacesuit_off], [`inside], [`empty]) state

val leave_base    : ([`lock_open], [`spacesuit_on], [`inside], 'pockets) state
                  -> ([`lock_open], [`spacesuit_on], [`outside], 'pockets) state 

val enter_base    : ([`lock_open], [`spacesuit_on], [`outside], [`full]) state
                  -> ([`lock_open], [`spacesuit_on], [`inside], [`full]) state

val moan          : ('lock, [`spacesuit_off], 'location, 'pockets) state
                  -> ('lock, [`spacesuit_off], 'location, 'pockets) state

val take_sample   : ([`lock_closed], [`spacesuit_on], [`outside], [`empty]) state 
                  -> ([`lock_closed], [`spacesuit_on], [`outside], [`full]) state

val empty_pockets : ('lock, 'suit, 'location, [`full]) state 
                  -> ('lock, 'suit, 'location, [`empty]) state

val end_day       : ([`lock_closed], [`spacesuit_off], [`inside], [`empty]) state -> unit

Я додав до проєкту простий Makefile, який дозволяє скомпілювати код за допомогою dockerized compiler, щоб ви могли за бажанням спробувати мій код, не встановлюючи собі весь toolchain Haskell або OCaml.

Як це роблять у мейнстрімі

Я сподіваюсь, що ви мені покажете :)

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

👍ПодобаєтьсяСподобалось4
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

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

{-# LANGUAGE FlexibleInstances #-}

module Moonbase2 where

import Control.Monad.Trans

newtype IxMonad a b m x = IxMonad {uM :: a -> m (x,b)}

ireturn :: Monad m => x -> IxMonad a a m x
ireturn x = IxMonad $ \s -> return (x,s)

(>>>=) :: Monad m => IxMonad a b m x -> (x -> IxMonad b c m y) -> IxMonad a c m y
abmx >>>= a_bcmy = IxMonad $ \a ->
  uM abmx a >>= \(b,x) ->
  uM (a_bcmy b) x

instance MonadTrans (IxMonad a a) where
  lift mx = IxMonad $ \a -> mx >>= \x -> return (x,a)

lift_io :: MonadIO m => IO x -> IxMonad a a m x
lift_io = lift . liftIO

data Opened = Opened
data Closed = Closed

data SuitOn = SuitOn
data SuitOff = SuitOff

data Inside = Inside
data Outside = Outside

data Empty = Empty
data Full = Full

do_beginday :: IxMonad a (Closed, SuitOff, Inside,Empty) IO ()
do_beginday =
  lift_io (print "day start") >>>= \() ->
  IxMonad $ \_ -> return ((), (Closed, SuitOff, Inside, Empty))

do_open_lock :: IxMonad (Closed,SuitOn,a,b) (Opened,SuitOn,a,b) IO ()
do_open_lock =
  lift_io (print "open door") >>>= \() ->
  IxMonad $ \(_,_,a,b) -> return ((), (Opened,SuitOn,a,b))

do_close_lock :: IxMonad (Opened,SuitOn,a,b) (Closed,SuitOn,a,b) IO ()
do_close_lock =
  lift_io (print "close door") >>>= \() ->
  IxMonad $ \(_,_,a,b) -> return ((), (Closed,SuitOn,a,b))

do_suiton :: IxMonad (Closed,SuitOff,Inside,Empty) (Closed,SuitOn,Inside,Empty) IO ()
do_suiton =
  lift_io (print "wearing a space suit") >>>= \() ->
  IxMonad $ \_ -> return ((), (Closed,SuitOn,Inside,Empty))

do_suitoff :: IxMonad (Closed,SuitOn,Inside,Empty) (Closed,SuitOff,Inside,Empty) IO ()
do_suitoff =
  lift_io (print "put off a space suit") >>>= \() ->
  IxMonad $ \_ -> return ((), (Closed,SuitOff,Inside,Empty))

do_leave_base :: IxMonad (Opened,SuitOn,Inside,Empty) (Opened,SuitOn,Outside,Empty) IO ()
do_leave_base =
  lift_io (print "leaving station") >>>= \() ->
  IxMonad $ \_ -> return ((), (Opened,SuitOn,Outside,Empty))

do_enter_base :: IxMonad (Opened,SuitOn,Outside,Full) (Opened,SuitOn,Inside,Full) IO ()
do_enter_base =
  lift_io (print "entering station") >>>= \() ->
  IxMonad $ \_ -> return ((), (Opened,SuitOn,Inside,Full))

take_sample :: IxMonad (Closed,SuitOn,Outside,Empty) (Closed,SuitOn,Outside,Full) IO ()
take_sample =
  lift_io (print "taking sample") >>>= \() ->
  IxMonad $ \_ -> return ((), (Closed,SuitOn,Outside,Full))

empty_pocket :: IxMonad (Closed,SuitOn,Inside,Full) (Closed,SuitOn,Inside,Empty) IO ()
empty_pocket =
  lift_io (print "taking sample") >>>= \() ->
  IxMonad $ \_ -> return ((), (Closed,SuitOn,Inside,Empty))

do_endday :: IxMonad (Closed,SuitOff,Inside,Empty) (Closed,SuitOff,Inside,Empty) IO ()
do_endday =
  lift_io (print "end ") >>>= \() ->
  IxMonad $ \_ -> return ((), (Closed,SuitOff,Inside,Empty))

myday =
  do_beginday >>>= \_ ->
  do_suiton >>>= \_ ->
  do_open_lock >>>= \_ ->
  do_leave_base >>>= \_ ->
  do_close_lock >>>= \_ ->
  take_sample >>>= \_ ->
  do_open_lock >>>= \_ ->
  do_enter_base >>>= \_ ->
  do_close_lock >>>= \_ ->
  empty_pocket >>>= \_ ->
  do_suitoff >>>= \_ ->
  do_endday
на с++ таке точно можна написати, я навіть щось таке подібне робив колись, з variadic templates, enable_if and partial template specification. блищить як сльоза дитини

Дякую за код!

Я навмисно не використовував монади, щоб не потрапити в ситуацію, коли усі коментарі будуть про те що «монади незрозуміло» або «монади непотрібно» :)

похоже на design by contract
не помню правда, было ли там что-то для компиляций?
если даже нет, то вероятнее всего для плюсов можно славать используя макросы

такий доу мені подобається

Як це роблять у мейнстрімі
Додатково є набір правил, виконання яких ми хочемо забезпечити на етапі компіляції

В мейнстрімі є юніттести, що фактично завжди є продовженням компіляції, і невід’ємною частиною процесу білда.
Якщо ж хочеться саме на етапі компіляції, то тут все просто — робимо класи з обмеженими інтерфейсами.
Ви не моделюєте на рівні об’єктів, по факту те що ви зробили (звалили всі дії в 1 клас) — це антипатерн (Год Обджект).
open_lock, enter_base, take_sample — це принаймні 2 різних класи — база, астронавт; ще там може бути сутність скафандр.
Якщо є бажання все на рівні саме компіляції, то робимо аналогічно, як це скоріше буде в ФП — скафандр та одягнений скафандр — це різні типи, так само як і база з відкритим та закритим люками. Якщо бізнес логіка, як у вас, одна строка, то купа класів — це оверхед, якщо маємо складну бізнес логіку, то можливо це й доцільно

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

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

А до чого тут юніттести?

Яку проблему вирішує

Додатково є набір правил, виконання яких ми хочемо забезпечити на етапі компіляції

?

Так от юніт тести вирішують рівно ту саму.

Ну, наприклад таку проблему:

Ви пишете client library до 3rd party сервісу, спілкування з яким іде по якомусь складному протоколу, з «додатковими правилами» типу «на цьому етапі можна посилати лише це, а на цьому — робити це і це», і т.п.

У моєму прикладі автор бібліотеки пише (не дуже складні) type annotations, і компілятор примушує виконувати ці правила.

Ви пропонуєте альтернативу: щоб користувачі цієї бібліотеки могли переконатись, що у них все гаразд з дотриманням правил, їм надо писати unit тести, щоб «конвертувати» runtime checks в build-time (або CI/CD time) checks. Але ж для цього їм потрібен або mock серверу, або mock API цієї бібліотеки, який моделює частину логіки серверу. Ці mock-и не можуть бути простими «затичками» — вони повинні мати свою внутрішню логіку, щоб відстежувати, в якому стані ми зараз є, що можна робити, а що — ні.

Ці mock-и користувачі мають або писати самі, або для них їх повинні написати ви. Але якщо готового mock-у нема, то скоріше за все користувачі не будуть писати їх самі.

Ви пишете client library до 3rd party сервісу, спілкування з яким іде по якомусь складному протоколу, з «додатковими правилами» типу «на цьому етапі можна посилати лише це, а на цьому — робити це і це», і т.п.

Це ми зараз про біржі говоримо? :)
Якщо ми пишемо клієнт зі сторони 3rd party сервісу, то так, нам би логічно обмежити, щоб абізяни не лізли з битими даними. Проблема в тому, що абізяни будуть писати на якійсь джаві/ЦПП/пітоні і не дуже зрадіють нашому дизайну клієнта (і там можуть знайтись винахідливі абізяни, що «імплементують протокол самостійно, бо ліба тормозить»). Нода (за рахунок тайпскріпта) тут трохи у виграшній позиції.
Якщо ми пишемо клієнт до сервісу, який ми не контролюємо, то компайлтайм перевірка нам не вирішить описану проблему, бо протокол може змінитись без відома автора бібліотеки.

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

то компайлтайм перевірка нам не вирішить описану проблему

Вона покращить ситуацію, тому що ми не зможемо зробити певний клас помилок. Якщо змінюється протокол, то ми отримаємо помилку protocol not supported.

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

Бо це не правда :)
Це зробити складно (не неможливо), але в реальному житті (точніше в мейнстрімі) це обходять іншими способами, що мають гірші показники по якісному атрибуту «надійність», але кращі по «ціна», «швидкість», «масштабування команди» (це скоріше вже зовсім бізнес метрика).

Так от юніт тести вирішують рівно ту саму.

Юніт-тести не вирішують проблему, вони просто збільшують ймовірність що все добре. Наприклад,

ISocket:
  IsConnected
  Connect(host, port)
  Send(data)

ми хочемо, щоб Send можна було викликати (1) після вдалого виклику Connect; (2) після вдалої перевірки IsConnected. В інших випадках помилка компіляції. Я розумію, що ми можемо розбити ISocket на

ISocket:
  IsConnected
  Connect(host, port)
та
IConnectedSocket:
  Send(data)

але чим тут допоможуть юніттести? Як вони можуть перевірити, що розробник десь в коді не викликав Connect без перевірки IsConnected?

Юніт-тести не вирішують проблему

Ви сформулюйте проблему спочатку :)

Даю підказку: оте, що ви написали про сокети — це приклад, а не формулювання загальної проблеми

Ви сформулюйте проблему спочатку :)

Сабж:

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

Ось, гарно сформульовано. Можливо треба додати «на етапі перевірки коду». Що тут незрозумілого?

Придумав маленьку задачу, яка зводиться до обмеження можливості використання тих чи інших функцій з API в залежності від того, які функціі було викликано до того.
Ось, гарно сформульовано. Можливо треба додати «на етапі перевірки коду»

Тут все зрозуміло, але сформульовано не гарно, навіть паршиво і зі спробою придумати (це ви правильно сказали) задачу від рішення, а не описати реальну проблему.

(це ви правильно сказали)

Це не я казав, це автор теми сказав.

задачу від рішення, а не описати реальну проблему

Я зрозумів, що для вас це не проблема.

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

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

Інший підхід у боротьбі з багами це типи. Причому типи можуть вживатися в двох значеннях, які інтуїтивно зрозумілі, але у мозку плутанина. Тому на співбесіді можна запитати, що таке тип даних (взагалі, а не в конкретній мові програмування), та подивитися на реакцію. Перше визначення типа це інформація, яка доступна лише до запуску кода (скажімо компіляція чи синтаксична перевірка), мета якої зменшення помилок в програмі. Зазвичай типи в Haskell та Сі це чисті приклади таких типів. Друге визначення це частина значення, яке описує те, що зберігається в інших. Наприклад, типи в Python це типовий приклад другого визначення. А з точки зору першого, в мові програмування Python усього один тип, це PyObject. Життя звісно складніше, і часто типи це суміш першого та другого визначення, наприклад об’єкти в C++ містять поля RTTI, тому в runtime ми можемо зробити певні перевірки. В тому ж Python є аннотації, є Pylint, ...

Так от, типи в (першому значенні) це як раз це один зі шляхів зменшити кількість багів. Наприклад, якщо ми звертаємося до неіснуючого поля в класі, то ми отримаємо помилку компіляції в Java або AttributeError під час виконання програми в Python. Чим більше розвинута система типів, тим більше різних помилок вона може піймати на етапі перевірки коду. Закінчується це все мовами програмування зі залежними типами (CoQ, Idris, Agda) де будь яке істинне математичне твердження стосовно поведінки програми можна довести засобами мови.

Якщо брати типи, то ми маємо множину скалярних типів, яку можна розвивати двома способами: OOP та ADT. Якщо брати OOP, то... воно зручне в написанні коду, але має певні проблеми з потужністю системи типів. Але в реальному житті на це просто забивають болт. Комусь вистачає Python, а комусь OOP: накрили юніттестами і ок, з’явився баг — винні тестери. Інколи, можливо, анотації та зовнішні системи. До речі, там є дуже цікава гілочка з верифікацією на базі трійок Хоара (Ada), але... якось це не дуже набуває популярності, тому це типовий випадок вирішення проблеми, коли вона не вважається реальною проблемою. ADT вже дає набагато більше гарантій, саме цим пояснюється популярність Rust.

Ну а далі... Як показує мій досвід, більшість розробників взагалі не уявляють, що таке взагалі можливо: ООП це топ, треба писати код, фіксити баги, це макисмум що можливо. Звісно що тут виникає бажання поділитися просвітити, що на світі також існує магія.

Інший підхід у боротьбі з багами це типи.

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

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

Мені жаль, що у вас такий рівень професіоналізму в вашому оточенні. Враховуючи стан універів (по всьому світу) далі буде тільки веселіше.
Але повертаємось до питання:
Який практичний ауткам ви очікуєте від того, що люди «дізнаються» (проникнуться) системою типів?

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

Ну... в Java немає розвинутої системи типів, тому як можна продемонструвати те, чого немає? Напевне, це можна зробити в Scala, але я її не знаю.

Взагалі дуже дивне питання. Аналогічно можна взяти проект наприклад, на Rust, та запитати, яка користь буде від Java ООП в ньому? Речення виглядає для мене як маячня.

так само нещодавно бачив на тайпскріпті спробу зрізати кути

Ну... якщо мова дозволяє «зрізати кути», то... рано чи пізно хтось це зробить. Можливо навіть не подумає що зрізає кут.

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

Бо ширпоживчі мови мають вбудовані обмеження.

Який практичний ауткам ви очікуєте від того, що люди «дізнаються» (проникнуться) системою типів?

На який практичний ауткам розраховують вчені, які пишуть науково-популярні книжки? Більше людей опанують, отримають задоволення, буде більш великий кругозір, ...

Мені жаль, що у вас такий рівень професіоналізму в вашому оточенні.

Ну... навіть на dou можна бачити, що більшість опонентів не розуміє, що таке монади :-)

Ну... навіть на dou можна бачити, що більшість опонентів не розуміє, що таке монади :-)

Так, я з цим теж часто стикаюсь, ось ви незодавно продемонстрували, що не маєте розуміння, що таке монада :)

На який практичний ауткам розраховують вчені, які пишуть науково-популярні книжки?

Бабло від продажів.
Але ваші вислови в цьому коментарі — це демагогії, тому коментувати всі не буду. Якщо в вас є бажання зрозуміти і донести свої ідеї, то перечитайте мій попередній коментар.

Бабло від продажів.

Малоймовірно, не кажучи про те, що часто це на волонтерских засадах.

Так, я з цим теж часто стикаюсь, ось ви незодавно продемонстрували, що не маєте розуміння, що таке монада :)

Де саме?

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

Але ваші вислови в цьому коментарі — це демагогії,

Демагогія на демагогію... В невідомому проекті, який вже написаний в ООП-стилі, щось треба показати... Щось нагадує ось будинок, треба поглибити фундамент на п’ять метрів.

Але це не гарантія того, що не існують сценарії, коли це валиться.

загалом юніт тести прямо для цього призначені by design

тести мають співпадати з тим, що дійсно треба від коду (подібну проблему мають застарілі коментарі в коді). тести має хтось писати. коду для тестів набагато більше, ніж опис типу (більше коду -> більше помилок, більше коду -> більше писати, більше платити програмісту). найголовніше тести by design не можуть бути повні, можуть бути повні лише для деякий випадків.

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

Мій приклад подобається мені тим, що навіть якщо користувач бібліотеки не хоче/не пише юніт-тести, то все одне некоректний код скомпілювати не вийде.

Якщо ж хочеться саме на етапі компіляції, то тут все просто — робимо класи з обмеженими інтерфейсами.

Покаже як?

Ви не моделюєте на рівні об’єктів, по факту те що ви зробили (звалили всі дії в 1 клас) — це антипатерн (Год Обджект).

Ну, в мене не ООП, тож є одна бібліотека/один модуль на все API, щоб код був простіший і основна ідея не загубилася за boilerplate. Але звичайно можна розбити мій тип State на чотири окремих типи, це суттєво нічого не змінить.

Якщо є бажання все на рівні саме компіляції

Є!

то робимо аналогічно, як це скоріше буде в ФП — скафандр та одягнений скафандр — це різні типи, так само як і база з відкритим та закритим люками

Ну, в мене тип один і той же, але з різними параметрами. Мені здається що це може бути суттєвою відмінністю. Як при цьому будуть виглядати типи аргументів для «будь-який стан скафандру», наприклад?

Якщо бізнес логіка, як у вас, одна строка, то купа класів — це оверхед, якщо маємо складну бізнес логіку, то можливо це й доцільно

Вважайте, що бізнес-логіка схована за інтерфейсом API, і користувачу не відомо, одна строка там, чи 100500. Бізнес-логіка тут не дуже цікава, фокус на дизайні API.

Я б залюбки подивився, як виглядатиме ваш варіант (побитий на окремі об’єкти, якщо так ідіоматично, і т.п.).

Мій приклад подобається мені тим, що навіть якщо користувач бібліотеки не хоче/не пише юніт-тести, то все одне некоректний код скомпілювати не вийде.

Користувач бібліотеки і не повинен писати юніт тести на бібліотеку. Ваш приклад має сутєву перевагу для такого сценарію — ви повідомляєте користувача __бібліотеки__ про можливі проблеми. Але для цього треба, щоб кодова база була організована певним чином. Якщо все в одному місці, то ассерт (навіть якщо там обмеження типів) буде прибрано, бо воно «заважало компіляції».

Покаже як?
Я б залюбки подивився, як виглядатиме ваш варіант (побитий на окремі об’єкти, якщо так ідіоматично, і т.п.).

Останні 2 реченні. А для того, щоб подивитись на код, то нам треба якось попасти на один і той самий реальний проект, можливо навіть будемо писати софт для бази на місяці.

Ну, в мене тип один і той же, але з різними параметрами. Мені здається що це може бути суттєвою відмінністю. Як при цьому будуть виглядати типи аргументів для «будь-який стан скафандру», наприклад?

Скоріше за все вони будуть в батьківському класі «Просто скафандр»

Ваш приклад має сутєву перевагу для такого сценарію — ви повідомляєте користувача __бібліотеки__ про можливі проблеми. Але для цього треба, щоб кодова база була організована певним чином. Якщо все в одному місці, то ассерт (навіть якщо там обмеження типів) буде прибрано, бо воно «заважало компіляції».

Я не зрозумів про «кодова база організована певним чином» — яким?

Про «буде прибрано» — я пропоную не доводити вже зовсім до абсурду, і прийняти, що користувач бібліотеки не буде стріляти собі в ногу, прибираючи safety checks.

Я б залюбки подивився, як виглядатиме ваш варіант (побитий на окремі об’єкти, якщо так ідіоматично, і т.п.).

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

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

Я не зрозумів про «кодова база організована певним чином» — яким?

Окремо винесені в інші репозиторії бібліотеки для початку. Але знову ж оновити версію — це час (якрах кілька тижнів тому довелось з цим погратись).
Для дизайну зовнішнього АПІ ви навели гарний аргумент. В контексті розробки сервісу (бізнес логіки процесу), в «мейнстрімі» для зменшення помилок покладаються на юніт тести (а по факту на ТТД-подібний процес розробки коду), бо це дозволяє дешевшими спеціалістами вирішувати задачу отримання певного низького рівня помилок при внесенні змін у функціональність.

Про «буде прибрано» — я пропоную не доводити вже зовсім до абсурду, і прийняти, що користувач бібліотеки не буде стріляти собі в ногу, прибираючи safety checks.

Бачив таке літералі тиждень тому :) Бо доставка фічі важливіша за дизайн АПІ, а «помилки тут немає, передаються коректні дані» (і в тому випадку все було коректно)

Я інтерпретую ваші коментарі так, що коду треба писати нетрівіальні обсяги (всяко більше моїх 30 строк),

1) Так. Я в принципі і не стверджував, що буде менше строк ніж у вас. Груба оцінка — буде десь 100-300 строк.
2) Якби навіть треба було лише 5 строк, то відповідь була б та сама

скафандр та одягнений скафандр — це різні типи, так само як і база з відкритим та закритим люками.

от за це я щиро люблю «класичний ооп» )) (на справді ніт)

... я сам «знайшов» цю «цікавинку» у «класичному ооп» буквально «на котах»

... бо «просто кіт» і «хворий кіт» то є буквально аж на стільки 2 різні стани «кота» що розглядати їх однаково нема жодного сенсу як то буквально просто сам по собі сам стан «хворий кіт» у випадку «просто кіт» просто відсутній як такий

... тож виходить «об’єкт» фактично може змінювати саме свій «тип» просто за свого існування

... чи може то за фактом «хворий кіт» виникає новий об’єкт вже іншого типу? а він наслідує «просто кота» чи ні? бо на вчили що «наслідування» то є «кіт та собака є тварини» але ні як собака може стати котом чи навпаки а потім повернутися знову на зад до «просто кота»

то купа класів — це оверхед

а от х.з. я ще тільки почав думати за це (років з 5 мабуть тому не обрахую по пам’яті) але досі не додумав як правильно треба і як взагалі треба і навіть як взагалі можна (ще)

Круто! Нарешті на ДОУ дійсно цікаві технічні статті! ;)

Я чисто для себе переходжу в деяких скриптах автоматизації з Python на Rust і ось це «так, стоп, не всі можливі варіанти оброблені» як помилка при компіляції ніби то і напрягає в Rust, бо в Python на це можна забити, але ж розуміння, що краще прописати повну логіку роботи, ніж падати в рантаймі мені і подобається. Хоча часу на написання виходить більше. Бо треба якісніше продумувати як всі можливі ситуації так і структури даних.

В С++ статически компилируемые стейт машины на основе параметризации шаблонов делали еще лет 20 назад. Правда сообщения об ошибках тогда были в килобайты и невнятные, но сейчас вроде поправили.

Эта задача очень похожа на стейт-машину, но она не про стейт-машины. То, что внутреннее состояние системы тут можно свести к стейт-машине — это просто так совпало.

Не очень понятно, что именно из (хорошего и эпохального) труда Александреску тут поможет :(

З чого би це, аналіз задачі що ви надали — це NFA тільки в якості API. Тобто API вашого програмного модуля не задає конкретного алгоритму з послідовностями дій, відповідно ви вимушені робити перевірки state вашого NFA безпосередньо в API, що звісно бажано зробити як найраніше особливо під час компіляції, тобто щоби це взагалі не впливало на runtime та при цьому так самоті користувачу API важко би було створити не коректний програмний код по реалізації конкретного алгоритму. Тобто метод відомий як декларативне мета програмування. А там вже різні методи і різних технологіях для цього є, шаблони і constexpr в С++, на цьому збудовано безліч фреймверків в Boost, зокрема практичні Spirit та Expressive.Також є два універсальні мета фреймверках старий MPL під С++ 03 та більш сучасний Hana під С++ 11+. додаткові прероцессори як то Aspect C++, або qmake у QT. Що в С++ із стандартом 20 додали концепти, які якраз вже дають можливість реалізувати описані вами рестрікшени якраз на рівні компіляції.
У Java є Aspect J, механізм Annotation preprocessor, що дозволяє обробляти ситуацію коли компілятор потрапляє на анотацію під час компілювання і т.п. на чому збудовані інструменти типу lombok та mapstruct, jekson і т.д.
Досліди у мета програмування продовжуються, бо це дуже потужний абстракційний інструмент, суттєво підвищуючий виробничі можливості програміста зменшуючи час відладки — найдорожчий по часу і зусиллям процесс, який при цьому безкоштовний на рантаймі.

О, специфіка. Дуже дякую.

А не покажете, як це робиться для цього іграшкового прикладу?

Ну ви звісно в курсі, що це такий самий іграшковий приклад як і Велика Теорема Ферма (інакше би не писали статтю про фішки Haskel та Rust). Ця штука це породження проблеми на мійльярди долларів відомої як невірне використання malloc/free та їх аналогів в різних формах. Тим не мнеше от вже розумний AI написав Call Order Enforcer — тобто це можливо, хоч і таке собі. Є інші методи побудови API, Resource Acquisition Is Initialization, fluent interface і т.д.
От тут розумний AI накидав, приклад.

#include <concepts>
#include <iostream>

// Type-level representation of states
struct NotInitialized {};
struct Initialized {};

// Concept to check if a type is Initialized
template <typename T>
concept InitializedState = std::is_same_v<T, Initialized>;

template <typename State>
struct CallOrderEnforcer {
    template <typename NextState = State>
    struct EnforcedCall {
        template <typename Func, typename... Args>
        requires InitializedState<State> // Constraint using requires clause
        auto operator()(Func&& func, Args&&... args) -> decltype(std::forward<Func>(func)(std::forward<Args>(args)...)) {
            return std::forward<Func>(func)(std::forward<Args>(args)...);
        }
    };

    template <typename NextState>
    CallOrderEnforcer<NextState> transition() const {
        return CallOrderEnforcer<NextState>();
    }

    EnforcedCall<> enforce() const{
        return EnforcedCall<>();
    }
};

void initialize() {
    std::cout << "Initializing...\n";
}

void foo() {
    std::cout << "Doing foo...\n";
}

void bar() {
    std::cout << "Doing bar...\n";
}

int main() {
    CallOrderEnforcer<NotInitialized> enforcer;

    auto initialized_enforcer = enforcer.transition<Initialized>();

    // These will work:
    initialized_enforcer.enforce()(initialize);
    initialized_enforcer.enforce()(foo);
    initialized_enforcer.enforce()(bar);

    // These will produce compile-time errors (with better error messages):
    // enforcer.enforce()(foo);       // Error: Constraints not satisfied
    // enforcer.enforce()(bar);       // Error: Constraints not satisfied
    // enforcer.enforce()(initialize); // Error: Constraints not satisfied

    CallOrderEnforcer<NotInitialized> another_enforcer;
    auto initialized_another_enforcer = another_enforcer.transition<Initialized>();

    initialized_another_enforcer.enforce()(initialize);
    initialized_another_enforcer.enforce()(foo);
    initialized_another_enforcer.enforce()(bar);

    return 0;
}

Дякую за код!

Як я бачу, тут не можна просто брати і використовувати foo і bar як звичані функції, їх треба обов’язково загортати в enforcer. Як ви правильно написали, «таке собі» :)

Так і у Haskell будє синтаксичний офрехед, щоби отримати перевірку під час компіляції.

{-# LANGUAGE GADTs #-}

data State = NotInitialized | Initialized

data CallOrderEnforcer s where
  NotInit :: CallOrderEnforcer NotInitialized
  IsInit :: CallOrderEnforcer Initialized

initialize :: IO () -> CallOrderEnforcer NotInitialized -> IO (CallOrderEnforcer Initialized)
initialize f _ = do
  f
  return IsInit

class Enforceable s m where
  enforce :: m a -> CallOrderEnforcer s -> m (CallOrderEnforcer s)

instance Monad m => Enforceable Initialized m where
  enforce action _ = action >> return IsInit

initialize_fn :: IO ()
initialize_fn = putStrLn "Initializing..."

foo :: IO ()
foo = putStrLn "Doing foo..."

bar :: IO ()
bar = putStrLn "Doing bar..."

main :: IO ()
main = do
  let enforcer = NotInit
  initialized_enforcer <- initialize initialize_fn enforcer
  _ <- enforce foo initialized_enforcer
  _ <- enforce bar initialized_enforcer

  -- The following will produce a type error:
  -- enforce foo enforcer
І у OCaml
(* Define state types *)
type not_initialized = Not_initialized
type initialized = Initialized

(* Define the call order enforcer module *)
module CallOrderEnforcer = struct
  (* Abstract type for the state *)
  type 's t

  (* Constructor for the NotInitialized state *)
  let not_init : not_initialized t = Obj.magic ()

  (* Initialize function transitions to Initialized state *)
  let initialize (f : unit -> unit) (_ : not_initialized t) : initialized t =
    f ();
    Obj.magic ()

  (* Enforce function, only available in Initialized state *)
  let enforce (f : unit -> unit) (_ : initialized t) : unit =
    f ()
end

(* Example functions *)
let initialize_fn () = print_endline "Initializing..."
let foo () = print_endline "Doing foo..."
let bar () = print_endline "Doing bar..."

let () =
  (* Correct usage *)
  let enforcer = CallOrderEnforcer.not_init in
  let initialized_enforcer = CallOrderEnforcer.initialize initialize_fn enforcer in
  CallOrderEnforcer.enforce foo initialized_enforcer;
  CallOrderEnforcer.enforce bar initialized_enforcer;

  (* The following will produce a type error at compile time: *)
  (* CallOrderEnforcer.enforce foo enforcer *)
Воно не сентакчисний сахар, тобто це плата за перевірку під час компіляції. Не краще справи у
D
import std.stdio;
import std.traits;

struct NotInitialized {}
struct Initialized {}

struct CallOrderEnforcer(State) {
    alias State thisState;

    auto initialize(alias F)() if (is(State == NotInitialized)) {
        F();
        return CallOrderEnforcer!(Initialized)();
    }

    void enforce(alias F)() if (is(State == Initialized)) {
        F();
    }
}

void initialize_fn() {
    writeln("Initializing...");
}

void foo() {
    writeln("Doing foo...");
}

void bar() {
    writeln("Doing bar...");
}

void main() {
    auto enforcer = CallOrderEnforcer!(NotInitialized)();
    auto initialized_enforcer = enforcer.initialize!initialize_fn();
    initialized_enforcer.enforce!foo();
    initialized_enforcer.enforce!bar();

    // The following will not compile:
    // enforcer.enforce!foo(); // Error: enforce is not a member of CallOrderEnforcer!(NotInitialized)

    auto enforcer2 = CallOrderEnforcer!(NotInitialized)();
    auto initialized_enforcer2 = enforcer2.initialize!initialize_fn();
    initialized_enforcer2.enforce!foo();
    initialized_enforcer2.enforce!bar();
}
Scala
sealed trait State
object NotInitialized extends State
object Initialized extends State

case class CallOrderEnforcer[S <: State](private val dummy: Unit = ()) {
  def initialize[U](f: => U): CallOrderEnforcer[Initialized.type] = {
    f
    CallOrderEnforcer[Initialized.type]()
  }

  def enforce[U](f: => U)(implicit ev: S =:= Initialized.type): U = {
    f
  }
}

object Main extends App {
  def initialize_fn(): Unit = println("Initializing...")
  def foo(): Unit = println("Doing foo...")
  def bar(): Unit = println("Doing bar...")

  val enforcer = CallOrderEnforcer[NotInitialized.type]()
  val initializedEnforcer = enforcer.initialize(initialize_fn())
  initializedEnforcer.enforce(foo())
  initializedEnforcer.enforce(bar())

  // The following will not compile:
  // enforcer.enforce(foo()) // Error: Cannot prove that NotInitialized.type =:= Initialized.type

  val enforcer2 = CallOrderEnforcer[NotInitialized.type]()
  val initializedEnforcer2 = enforcer2.initialize(initialize_fn())
  initializedEnforcer2.enforce(foo())
  initializedEnforcer2.enforce(bar())
}
та Rust
struct NotInitialized;
struct Initialized;

struct CallOrderEnforcer<State> {
    _state: std::marker::PhantomData<State>,
}

impl CallOrderEnforcer<NotInitialized> {
    fn initialize<F>(f: F) -> CallOrderEnforcer<Initialized>
    where
        F: FnOnce(),
    {
        f();
        CallOrderEnforcer {
            _state: std::marker::PhantomData,
        }
    }
}

impl CallOrderEnforcer<Initialized> {
    fn enforce<F>(f: F)
    where
        F: FnOnce(),
    {
        f();
    }
}

fn initialize_fn() {
    println!("Initializing...");
}

fn foo() {
    println!("Doing foo...");
}

fn bar() {
    println!("Doing bar...");
}

fn main() {
    let enforcer = CallOrderEnforcer::<NotInitialized> {
        _state: std::marker::PhantomData,
    };

    let initialized_enforcer = enforcer.initialize(initialize_fn);
    initialized_enforcer.enforce(foo);
    initialized_enforcer.enforce(bar);

    // The following will not compile:
    // enforcer.enforce(foo); // Error: No method named `enforce` found for struct `CallOrderEnforcer<NotInitialized>`

    let enforcer2 = CallOrderEnforcer::<NotInitialized> {
        _state: std::marker::PhantomData,
    };
    let initialized_enforcer2 = enforcer2.initialize(initialize_fn);
    initialized_enforcer2.enforce(foo);
    initialized_enforcer2.enforce(bar);
}
«Таке собі» не тому що додатковий код, а тому що у підходу багато пересторог, наприклад це працюватиме лише в рамках одного модуля тарнсляції, обламається на вкладених функціяї і і т.д. Для таких штук треба більш розвинені інструменти в мовах програмування і в ідеалі інтеграція AI в засоби розробки. Це вже перехід на мови над високого рівня, певним чином.

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

Haskell:

{-# LANGUAGE DataKinds #-}

data InitState = NotInitialized | Initialized
data State (initState::InitState) where State :: State initState

not_initialized :: () -> IO (State NotInitialized) 
not_initialized () = return State
  
initialize_fn :: State NotInitialized -> IO (State Initialized) 
initialize_fn _ = do
  putStrLn "Initializing..."
  return State

foo :: State Initialized -> IO (State Initialized)
foo _ = do 
  putStrLn "Doing foo..."
  return State
  
bar :: State Initialized -> IO (State Initialized)
bar _ = do
  putStrLn "Doing bar..."
  return State
  
main :: IO ()
main = do
  not_initialized_state <- not_initialized ()
  initialized <- initialize_fn not_initialized_state
  state <- foo initialized
  state <- bar state
  -- The following will produce a type error:
  -- state <- foo not_initialized_state
  return ()

І OCaml:

type 's t = State

let not_initialized : unit -> [`not_initialized] t = fun () -> State
    
(* Example functions *)
let initialize_fn  : [`not_initialized] t -> [`initialized] t = fun _ -> print_endline "Initializing..."; State
let foo : [`initialized] t -> [`initialized] t = fun _ -> print_endline "Doing foo..."; State
let bar : [`initialized] t -> [`initialized] t = fun _ -> print_endline "Doing bar..."; State

let () =
  (* Correct usage *)
  let not_initialized = not_initialized () in
  let state = initialize_fn not_initialized in
  let state = foo state in
  let state = bar state in
  (* The following will produce a type error at compile time: *) 
  (* let state = foo not_initialized in *)
  ()

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

Ви пишете: "Для таких штук треба більш розвинені інструменти в мовах програмуванняч. Мій message в тому, що цей велосипед вже винайдено, можна брати та їхати, нічого додаткового вигадувати не треба :)

За великим рахунком, так. Берем Java та додатковий інструмент AspectJ який глобально розповсюджений. Маємо от такий аспект

@Aspect
public class CallOrderAspect {

    private static boolean isInitialized = false;

    @Pointcut("execution(* MyClass.initialize(..))")
    public void initializeCall() {}

    @Pointcut("execution(* MyClass.foo(..)) || execution(* MyClass.bar(..))")
    public void restrictedMethods() {}

    @Before("initializeCall()")
    public void beforeInitialize(JoinPoint jp) {
        isInitialized = true;
    }

    @Before("restrictedMethods() && !within(CallOrderAspect)")
    public void checkInitialization(JoinPoint jp) {
        if (!isInitialized) {
            throw new IllegalStateException("Must call initialize() before " + jp.getSignature().getName());
        }
    }
}
От тестовий код
public class MyClass {
    public void initialize() {
        System.out.println("Initializing...");
    }

    public void foo() {
        System.out.println("Doing foo...");
    }

    public void bar() {
        System.out.println("Doing bar...");
    }

    public static void main(String[] args) {
        MyClass obj = new MyClass();
        obj.initialize();
        obj.foo();
        obj.bar();

        // The following would cause an error (at compile time with AspectJ):
        // MyClass obj2 = new MyClass();
        // obj2.foo();
    }
}
Це усе не можна зробити, інтерпритаціних мовах, бо нема фази компіляції — та таж можна викорастати скажімо лінтер типу ESLint.

Ага а ще про пітон і компіляцію. Про що ти автор взагалі?

Python — популярна ligua franca для прикладів (як я її і використав), яка не компілюється (про що я і зазначив). З іншого боку, мови, що компілюються, можуть робити певні перевірки на етапі компіляції, особливо якщо у них є розвинена type system (про що і стаття).

Такий TLDR простіше опанувати?

Ну ок є спец ліба у С++ у boost з метапрограмуванням. Але сенс? Якщо можна запрограмувати у вигляді таблиці/бітовими флагами і перевіряти в рантаймі але без бісової магії і купи бібліотечного коду

Якщо можна запрограмувати у вигляді таблиці/бітовими флагами і перевіряти в рантаймі але без бісової магії і купи бібліотечного коду

Бісова магія та купа коду — це в С++ так буде? Бо в моїх прикладах якось обійшлося без купи коду (і без бісової магії)...

Звісно, в рантаймі перевіряти можна (і так і роблять в 9 випадках із 10), але допис мій саме про те, що є способи робити це в compile time, і це може бути просто, і лаконічно, і не тягти за собою «бісову магію і купу коду», і взагалі — за кордонами С++ є життя, і воно непогане :)

С++ звісно. Типізація тут про магію шаблонів ... і кучу коду... хоч і переважно з бібліотеки

А що якщо результат виконання залежить від рантайм умов? Наприклад генерується рандомне число, і в залежності від цього числа викликається функція. У такому випадку на етапі компіляції програма не може знати порядок виклику функцій, бо конкретний порядок виклику буде визначений тільки під час виконання. Тобто компілятору теоретично треба буде розглянути всі варіанти виконання, щоб визначити які варіанти є некоректними. Це чимось схоже, на мою думку, на uk.wikipedia.org/wiki/Проблема_зупинки. Це я веду до того, що мені здається, що це неможливо зробити у загальному випадку

А що якщо результат виконання залежить від рантайм умов? Наприклад генерується рандомне число, і в залежності від цього числа викликається функція.

«Скажи мені, що ти не читав мій код, не кажучи, що ти не читав мій код» :) Саме це (використання random) є в одному з прикладів використання:
github.com/...​aml/adept/main.ml#L29-L50

У такому випадку на етапі компіляції програма не може знати порядок виклику функцій, бо конкретний порядок виклику буде визначений тільки під час виконання.

Компілятор може дійти висновку, що два шляхи виконання після random приводять до різних типів, і якщо вони повинні бути однакові (скажімо, це два бранча в if, або щось таке), то це буде помилка, як в наведеному мною прикладі вище

Ну... якщо в результаті виконання програми типи гарантують тобі, що шлюз відкритий, ти можеш його закрити. Якщо в результаті виконання програми типи не гарантують тобі цього, то тобі треба додатково вставити перевірку, чи відкритий шлюз, і вже у середині if/case/pattern matching у тебе будуть гарантії того, що шлюз відкритий, і ти зможеш його закрити.

Теоретично все супер, але є така штука як фізика і радіація у будь-який момент може зробити bit flip в пзу і програма може опинитись у невалідному стані, хоча наче і прописаний кінцевий автомат не дозволяє зайти у неправильний стан, тому обробка нештатних станів в космічному пз робиться інакше. Але для звичайного пз підхід доволі непоганий, хоча й доволі вербозний і при великій кількості станів складність коду зростає експоненціально

хоча й доволі вербозний

Тобто 1 строчка на одну Api фунцію то забагато, і треба було менше? :)

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

розумний компілятор може зіграти злий жарт — він побачить що гіпотетично код недосяжний і тупо викине обробку нештатних ситуацій тому насправді мені було б цікаво почитати як пишуть програми що можуть протистояти радіаційному впливу на оперативку

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

По-друге, вже існують радіаційно стійки процесори з дублюванням апаратних операцій.

Підписатись на коментарі