Трохи балачок про assert-и: еволюція підходів від C до залежних типів

19 листопада 2024 року понад 2 мільйони вебсайтів одночасно впали. Не через хакерську атаку. Не через перевантаження серверів. А через один-єдиний assert! у Rust-коді Cloudflare.

Обговорень було море. Але, як це часто буває, більшість з них швидко перетворилися на холівар. За цими баталіями загубилося головне: цікаве технічне питання про те, як різні мови програмування підходять до assert-ів. Це і спонукало мене написати цю статтю.

Вступ

Тож що ж таке ці assert-и? Вони є майже в кожній мові програмування. У C це assert(condition), у Python теж assert condition, у Java assert condition : message. Rust пропонує цілу низку варіантів: assert!(), debug_assert!(), assert_eq!(), assert_ne!(). C# має Debug.Assert() та Trace.Assert(). Ada пропонує pragma Assert. Навіть JavaScript, який довгий час обходився без них у стандарті, отримав їх через Node.js (assert модуль).

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

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

Головне призначення assert-ів полягає у тому, щоб допомогти знайти помилки під час розробки та тестування. Вони кричать нам: «Стоп! Тут щось пішло не так! Твої припущення неправильні!» Це інструмент для виявлення багів на ранніх стадіях, коли їх ще легко виправити.

Які бувають assert-и

Найпростіший варіант, який всі бачили, це звичайний assert(condition). Декларуємо умову, яка має виконуватися. Просто і зрозуміло.

Але в розрізі нашого визначення твердження цим не обмежуються. У Rust, наприклад, є unwrap() та expect(). Ці методи містять assert як складову частину їхньої логіки.

Твердженнями також є pre-conditions та post-conditions. Це умови, які мають виконуватися до та після виклику функції. Ada дозволяє описати їх прямо в сигнатурі функції:

function Find_Max (A : My_Array_Type) return Integer with
  Pre  => A'Length > 0,
  Post => (for all J in A'Range => Find_Max'Result >= A(J)) and
          (for some J in A'Range => Find_Max'Result = A(J));

Тут Pre => A'Length > 0 це твердження, яке буде перевірено перед викликом функції: масив не може бути порожнім. А післяумова складніше та навіть містить квантори, про які можна думати як про цикли перевірок. Перший рядок переконується, що результат не менший за всі елементи. Другий рядок — що результат дорівнює якомусь елементу масиву. Такий контракт майже не залишає простору для неправильної реалізації функції.

Є ще більш екзотичний варіант: інваріанти класу. Це твердження, які мають виконуватися завжди, коли об’єкт знаходиться в «стабільному» стані. Синтаксичний цукор, щоб не вставляти один і той же assert на початку та в кінці кожного методу. В Eiffel це була центральна концепція Design by Contract:

class sorted_pair
feature
  first, second: integer

  set(a, b: integer)
    local
      temp: integer
    do
      first := a
      second := b
      -- тут інваріант може бути порушено!
      if first > second then
        temp := first
        first := second
        second := temp
      end
    end

invariant
  first <= second
end

Інваріант first <= second автоматично перевіряється на вході та виході з кожного публічного методу. Всередині методу set він може тимчасово порушуватися, бо на початку ми просто присвоюємо значення з довільних аргуменів. Але на виході з методу інваріант гарантовано відновлюється. Так, це було необов’язково, але ми це зробили більше для демонстрації ідеї.

Аналогічно до інваріантів класу існують і інваріанти циклу. Це твердження, яке має виконуватися на початку та в кінці кожної ітерації циклу.

Поведінка assert-ів

З поведінкою assert-ів під час розробки все більш-менш зрозуміло. Їхня головна мета полягає в допомозі нам, розробникам. Коли твердження хибне, програма має негайно зупинитися і надати максимум інформації (файл, рядок, умова, стек), щоб розробник швидко зрозумів, що пішло не так. Це вірний помічник, який б’є на сполох при перших ознаках проблеми.

Але що робити, коли код потрапляє у прод? Коли «ситуація, якої ніколи не мало статися» раптом трапляється на бойовому сервері? Ось тут і з’являється вибір. Подивимося, які основні варіанти існують.

Варіант 1: Ігнорувати проблему і сподіватися на краще

Найперший і, мабуть, найпростіший підхід полягає в тому, щоб у релізній версії коду просто вимикати всі перевірки. Багато мов програмування, як от C++, роблять це за замовчуванням. Логіка тут проста: assert-и потрібні для розробки, а в продакшені вони лише сповільнюють роботу. Колись, за часів динозаврів та повільних процесорів, це був вагомий аргумент. Сьогодні ж це вже звучить не так переконливо.

Що відбувається, коли такий assert спрацював би, але його немає? Програма опиняється в стані, на який вона не була розрахована. Це дуже слизька доріжка, що веде прямісінько до горезвісного «Undefined Behavior» (UB). Програма може мовчки видавати неправильні результати, пошкодити дані або впасти значно пізніше, коли знайти причину буде майже неможливо.

Але чи завжди це погано? Іноді дивна поведінка краща за падіння. Уявіть собі комп’ютерну гру. Нехай буде стара добра Jagged Alliance 2. Там характеристики персонажів були в діапазоні від 0 до 100. Але через різні бонуси та модифікатори непомітність бійця могла перевищити 127. Це призводило до переповнення char, і наш ніндзя раптом ставав помітніше за стадо буйволів. Це смішно, це баг, але це краще, ніж якби гра просто вилетіла наприкінці довгої битви, яку ви не зберегли. Інколи краще глюк на екрані, ніж повна зупинка програми. Користувач може не помітити дивну поведінку, але креш помітять усі. До того ж наш світ без глітчей та спідранів був би трошечки сумнішим.

Варіант 2: Завжди падати (Rust-стиль)

Повною протилежністю ігноруванню буде нещадне падіння програми як тільки так відразу. Це своєрідна філософія, яку активно сповідує Rust. Тут логіка така: якщо ми дійшли до стану, який неможливий за нашими припущеннями, це означає, що програма знаходиться в некоректному стані. Продовження роботи в такому стані може призвести до непередбачуваних наслідків, тобто того самого «Undefined Behavior». Краще вже впасти відразу, ніж потім ламати голову над причинами дивних помилок.

Саме це і сталося 19 листопада 2024 року з Cloudflare. Assert спрацював, демон впав, а з ним і 2+ мільйони сайтів. Технічно все правильно: система виявила невалідний стан і зупинилася замість того, щоб ризикувати. Але для кінцевого користувача це нагадує сумний анекдот: «Чи потів хворий перед смертю?». З точки зору користувача результат той самий: програма не працює.

Варіант 3: «Золота середина» — підміняємо логіку

Між двома крайнощами — ігноруванням і падінням — існує ціла низка проміжних варіантів. Замість класичного assert, який або зникає, або все валить, ми можемо «підкласти» під помилкову ситуацію якусь іншу логіку. Часто, ще з часів Delphi, це реалізується через механізм винятків (exceptions). Інколи можна побачити й кастомні макроси на кшталт ASSERT_WITH_CODE, що виконують певний код при спрацьовуванні. По суті це можна розглядати як визнання того, що assert-и на проді є небажаними, і будь-яку потенційну помилку краще обробляти явно.

На перший погляд це може здатися ідеальною «золотою серединою». Програма не падає, а продовжує роботу, намагаючись коректно відреагувати на несподівану ситуацію. Проте тут криються свої підводні камені. Нам доводиться писати обробники помилок для ситуацій, які, за нашими припущеннями, ніколи не мають виникнути. Такий код часто буває важко протестувати. Щоб досягти 100% покриття тестами, доводиться вигадувати складні «моки» на assert-и або йти на інші хитрощі. Іноді при помилці потрібно нетривіально відкотити всі зміни, що вже були зроблені.

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

Зазвичай у таких сценаріях ми зазвичай якось логуємо випадок, «прокидаємо» помилку нагору по стеку, щоб її обробив вищий рівень програми. Але буває й так, що помилка просто не вписується в бізнес-логіку. Наприклад, при авторизації користувача нам потрібна бінарна відповідь: «дозволено» або «не дозволено». Будь-яка відповідь при помилці буде поганою. Якщо ми відповімо «ні», для користувача це виглядає як непрацююча система. Якщо ми відповімо «так», то це ще гірше: система не працює з точки зору безпеки.

Варіант 4: Математика на службі

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

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

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

SMT-солвери: автоматичні математики

Один зі способів це делегування роботи спеціальним інструментам, які називаються SMT-солверами (Satisfiability Modulo Theories). Якщо простими словами, то це така розумна програма, яка вміє автоматично доводити або спростовувати математичні твердження. Яскравий приклад реалізації цього підходу — мова Ada разом з інструментом SPARK.

Пам’ятаєте приклад коду на Ada з pre- та post-умовами для функції Find_Max? SPARK працює так: він «згодовує» SMT-солверу всі pre-умови вашої функції як початкові факти. Потім він аналізує код рядок за рядком, додаючи нові факти. Наприклад, якщо у вас є факт, що X > 5, а в коді написано Y := X + 1, то солвер робить висновок, що Y > 6. Коли SPARK зустрічає assert(Y > 0), він намагається довести його істинність на основі вже відомих фактів. Якщо йому це вдається, перевірка вважається математично доведеною, тож її можна сміливо викинути з рантайму.

Звучить круто, правда? Але є нюанси. По-перше, SMT-солвер не всесильний. Існує цілий клас задач, які він не може довести автоматично. По-друге, щоб допомогти солверу, програмісту часто доводиться додавати проміжні assert-и, які підказують правильний хід думок. SMT-солвер не впорався? Додамо ще assert. Ще раз не впорався? Ще assert. Ще раз? Треба думати...

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

Залежні типи: сам собі математик

Якщо SMT-солвери це складно, то тримайтеся, бо ми переходимо до важкої артилерії. Це мови програмування із залежними типами, такі як Idris, Agda або Coq. Вони інтегрують математичні докази безпосередньо в систему типів.

У звичайній мові ми можемо написати тип «масив цілих чисел». У мові з залежними типами можна написати «масив з рівно 10 цілих чисел». В C++ теж так можна! Але йдемо далі, «масив довжиною N», де N це змінна або параметр функції. Або навіть «відсортований масив цілих чисел».

Тип стає математичним твердженням, це всім відомий ізоморфізм Карі-Говарда. Програма містить докази цих тверджень. Якщо компілятор прийняв код, значить, доказ коректний.

Звісно, є «невеличкий» нюанс: цей доказ доведеться побудувати власноруч. Якщо SMT-солвер робить докази автоматично: ви пишете код, він намагається довести, то тут усе навпаки: ви маєте побудувати доказ власноруч. Компілятор лише перевіряє, чи доказ коректний.

І це, м’яко кажучи, непросто. Це вимагає від програміста бути не просто інженером, а ще й трішки математиком. А це, погодьтеся, досить рідкісний і високооплачуваний скіл.

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

Давайте подивимося на невеличкий приклад мовою Agda. Припустимо, ми хочемо написати функцію, яка бере два непорожні масиви (вектори в C++ або списки в Python), з’єднує їх і повертає другий елемент результату.

-- Takes two non-empty vectors and returns the second element of their concatenation
second : {A : Set} {n m : ℕ} → 𝕍 A (suc n) → 𝕍 A (suc m) → A
second {A} {n} {m} vec1 vec2 =
  let one = suc zero
      concatenated : 𝕍 A (suc n + suc m)
      concatenated = vec1 ++ vec2
      -- Proof that index 1 is in bounds
      index-proof : one < suc n + suc m
      index-proof = 1<suc-n+suc-m
  in nth concatenated one index-proof

Що ми бачимо? Функція second приймає аж п’ять аргументів. Перші три — {A : Set} {n m : ℕ} — записані у фігурних дужках. Це неявні аргументи, такі собі параметри за замовчуванням. Тільки на відміну від більшості мов програмування, їх значення не задає програміст, а виводить компілятор.

A — це аргумент типу Set, який містить типи. Для педантів поясню, що Set містить типи першого порядку, бо тип усіх типів призводить до парадоксу Расела. Тобто A задає тип елементів масиву (чи то числа, чи рядки, чи щось інше).

n та m — це натуральні числа, довжини масивів, точніше, довжина мінус один, бо реальна довжина suc n та suc m.

А далі — це два аргументи, які нам треба задавати явно. Перший — 𝕍 A (suc n) — масив довжини n + 1. Другий — 𝕍 A (suc m) — масив довжини m + 1. Тут suc це наступне натуральне число. Оскільки n та m — це натуральні числа (можуть бути нулем), то довжини масивів щонайменше 1. А значить обидва масиви не можуть бути порожніми, бо мінімальна довжина завжди 1. Компілятор не дозволить передати порожній масив — це буде помилка типу.

Коли ви викликаєте second vec1 vec2, компілятор дивиться на типи vec1 та vec2 і сам розуміє: Ага, A — це тип Int (вбудований тип для цілих, не плутайте з ℕ — математичним типом натуральних чисел, які існують на етапі компіляції), n = 5, m = 3. Схоже на generics у Java або templates у C++, тільки набагато потужніше.

Тіло функції second це дві дії: конкатенація масивів (нічого перевіряти, завжди можемо виконати) та отримання другого елемента, який має індекс 1. Для цього ми викликаємо функцію nth, і тут найцікавіше:

nth : {A : Set} {n : ℕ} → 𝕍 A n → (i : ℕ) → i < n → A
nth []       i       ()
nth (x ∷ xs) zero    (z<s)     = x
nth (x ∷ xs) (suc i) (s<s i<n) = nth xs i i<n
Подивімось на сігнатуру цієї функції. Тут теж п’ять аргументів! Спочатку A — тип елементів масиву. Потім n — довжина масиву. Потім сам масив. Потім індекс. Останній аргумент має тип i < n, тобто там має бути доказ того, що i завжди менше довжини масиву! І ми передаємо там лему 1<suc-n+suc-m, і так, це такий ідентифікатор з символами менше, плюс та мінус!

Ще раз, подумаймо про індекси. Щоб взяти другий елемент, нам треба передати доказ, що довжина масиву більше одиниці. Перший масив має довжину n+1, другий — m+1, конкатенація має довжину n+m+2. Інтуїтивно очевидно, що довжина масиву concatenated більше одиниці. Але... Agda потребує формальний доказ цього факту! Ми приведемо тут його, щоб трошечки залякати читача:

subst : {A : Set} {x y : A} (P : A → Set) → x ≡ y → P x → P y
subst P refl px = px

sym : {A : Set} {x y : A} → x ≡ y → y ≡ x
sym refl = refl

+-suc : (n m : ℕ) → n + suc m ≡ suc (n + m)
+-suc zero    m = refl
+-suc (suc n) m = subst (λ k → suc n + suc m ≡ suc k) (+-suc n m) refl

1<suc-n+suc-m : {n m : ℕ} → suc zero < suc n + suc m
1<suc-n+suc-m {n} {m} = s<s (subst (λ k → zero < k) (sym (+-suc n m)) z<s)

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

Красиво? Безумовно. Складно? Ще й як.

Погляньте на код: половина його — це не бізнес-логіка, а побудова доказів. Леми +-suc та 1<suc-n+suc-m — це математичні конструкції. Щоб писати такий код, треба розуміти конструктивну математику, теорію типів та мати терпіння святого.

example.agda

Підсумки: повертаємося до Cloudflare

Отже, ми пройшли довгий шлях від простих assert-ів до залежних типів. Тепер повернімось до того, з чого почали: до інциденту з Cloudflare. Чи можна було його уникнути, і як виглядало б ідеальне рішення?

На мій погляд, ключ до розв’язання цієї проблеми лежить у чіткому розділенні двох світів: Небезпечного зовнішнього світу та безпечного внутрішнього. Ми цієї теми не торкалися, це монада IO. Якщо функція повертає цей тип даних, це означає, що вона працює зі зовнішнім світом: читає конфігураційний файл, працює з базою даних, робить мережевий запит. Тут може піти не так абсолютно все: файл пошкоджено, база недоступна, у JSON-і не ті поля, тому така функція має бути параноїком.

Монада IO маркує такий код. Вона натякає, що це місце, де треба повертати Maybe Config. Це нотація Haskell, для інших, можливо, зрозуміліше буде Option<Config>.

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

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

Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось5
До обраногоВ обраному2
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

1. Принципово: не все можна довести математично. У багатьох випадках ви закладаєтесь на коректність виконання чогось в API — від сисколлів ядра і до викликів якогось REST, а воно починає робити те, що не дозволено. Ви не зможете всю ОС покрити математичною верифікацією.

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

2. Ще один варіант, що робити з assertʼами, це лоґувати спрацювання. Реально, велика практична система так робить навіть коли пропускає помилки далі. Та хоч лічильник інкрементувати, якщо лоґ не пишеться.

А так — звісно, корисно робити так, щоб максимально покласти на якийсь інструмент (буде він в компіляторі, лінтері, іншому статичному аналізаторі — вже деталі) пошук помилок, додавши твердження, які такий аналізатор зможе перевіряти.

PS: Поки писав, Денис прокоментував схожими тезисами. Ну це добре.

PPS: Відредаґуйте форматування, місцями всякі <div> вилазять прямим текстом.

Ви не зможете всю ОС покрити математичною верифікацією.

Вже зроблено у seL4.

Ще один варіант, що робити з assertʼами, це лоґувати спрацювання.

Ну... це в принципі підпадає під варіант 3, підміну логіки, можна логувати, можна перезапустити операцію/worker, можна повернути помилку, ...

Вже зроблено у seL4.

Бачив. Виняток, який підкреслює існування правила. Ніхто не здатен зробити таке з реальним ядром розмаху Linux чи Windows включаючи драйвери, одна NVidia дає стільки складности, що десятиліття уйдуть на його покриття.

Так що залишається тільки вірити і тестувати.

Саме тому на критичних системах в літаку ніколи не буде Linux. seL4 має свою нішу: системи реального часу, медичне обладнання, військова техніка, автопілоти...

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

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

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

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

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

Ще один варіант, що робити з assertʼами, це лоґувати спрацювання.

Так, додав, тепер це виглядає так

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

Виправив в одному місці

Трохи випадкових зауважень:

Отут ще трохи розповідають за користь асертів en.wikipedia.org/...​iki/Offensive_programming

В принципі, за моїм досвідом, велика кількість асертів (скажімо, 10% від коду) та талановитий manual QA в некритичних доменах надають кращу якість продукту і з меншими витратами часу, ніж написання юніт- та автотестів.

Здається, в Пітоні асерти імплементовані через ексепшни.

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

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

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

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

В Расті просто написали код, котрий швидше було написати blog.cloudflare.com/...​age/#memory-preallocation Без явного асерту, покладаючись на розрекламовану надійність Расту та те, що його компілер зловить проблеми.

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

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

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

най вона вважатиметься надлишковою, бо доведено що це ніколи не станеться

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

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

Haskell з 90-х років як раз, монада IO саме для контролю цього й була додана.

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