Як обробити ділення на нуль мовою, яка не підтримує винятків?


62

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

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

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

  1. Ігноруйте помилку і отримайте 0результат. Реєстрація попередження, якщо можливо.
  2. Додайте NaNяк можливе значення для чисел, але це ставить питання про те, як обробляти NaNзначення в інших областях мови.
  3. Припиніть виконання програми та повідомте користувачеві про сильну помилку.

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

Які є мої альтернативи поводженню помилки з поділом на нуль та які ризики стосуються варіанта №1.


12
якщо ви додали підтримку винятків, і користувач її не впіймав, тоді у вас буде варіант № 3
храповик виродка

82
Мені цікаво, яка дурна вимога вимагатиме від вас абсолютно нової мови програмування? З мого досвіду, кожна мова коли - небудь створений відстій (в конструкції або у виконанні, часто в обох) , і вона займає невиправдано багато зусиль , щоб навіть отримати , що багато. Є кілька винятків для першого, але не для другого, і оскільки вони легко <0,01% випадків, вони, ймовірно, помилки вимірювання ;-)

16
@delnan більшість нових мов створені для того, щоб можна було відокремити правила бізнесу від того, як вони реалізовані. Користувачеві не потрібно знати, як reject "Foo"було реалізовано, а просто те, що він відхиляє документ, якщо він містить ключове слово Foo. Я намагаюся зробити мову якомога легшою для читання, використовуючи терміни, якими користувач знайомий. Надання користувачеві власної мови програмування дає їм змогу додавати ділові правила, не залежно від технічного персоналу.
Реакційний

19
@Mathew Foscarini. Ніколи і ніколи не ігноруйте помилку і мовчки повертайте 0. Коли ви робите поділ, 0 може бути цілком законним значенням (чомусь у Power Basic є така річ, і це справді біль). Якщо ви поділяєте числа з плаваючою комою, Нан або Інф буде добре (подивіться на IEEE 754, щоб зрозуміти, чому). Якщо ви розділите цілі числа, ви можете зупинити програму, ділити на 0 ніколи не слід допускати (ну, якщо тільки ви не хочете реалізувати справжню систему виключень).

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

Відповіді:


98

Я б настійно радив проти №1, оскільки просто ігнорування помилок - небезпечна анти-модель. Це може призвести до важких для аналізу проблем. Встановлення результату ділення на нуль на 0 не має сенсу, а продовження виконання програми з безглуздим значенням спричинить проблеми. Особливо, коли програма працює без нагляду. Коли інтерпретатор програми помічає, що в програмі є помилка (а поділ на нуль майже завжди є помилкою в дизайні), її переривання та зберігання все як є, як правило, перевагу над заповненням вашої бази сміттям.

Крім того, ви навряд чи матимете успіх, ретельно дотримуючись цю схему. Рано чи пізно ви зіткнетеся з ситуаціями помилок, які просто неможливо проігнорувати (наприклад, не вистачає пам'яті або переповнення стека), і вам доведеться реалізувати спосіб припинення програми в будь-якому випадку.

Варіант №2 (використання NaN) буде нелегкою роботою, але не настільки, наскільки ви можете подумати. Як поводитися з NaN в різних обчисленнях, добре зафіксовано в стандарті IEEE 754, тож ви, ймовірно, можете просто робити те, на чому написаний перекладач.

До речі: Створення мови програмування, що використовується непрограмістами, - це те, що ми намагалися зробити з 1964 року (Dartmouth BASIC). Поки ми були невдалі. Але удачі все одно.


14
+1 спасибі Ви переконали мене в помилці, і тепер, коли я прочитав вашу відповідь, я не розумію, чому я вагався. PHPпогано вплинув на мене.
Реакційний

24
Так, є. Коли я прочитав ваше запитання, я одразу подумав, що це дуже важлива річ PHP: виробляти неправильний висновок і продовжувати перевіряти, на тлі помилок. Є вагомі причини, чому PHP є винятком у цьому.
Джоель

4
+1 за коментарем BASIC. Я не раджу користуватися NaNмовою для початківців, але в цілому чудова відповідь.
Росс Паттерсон

8
@Joel Якби він прожив досить довго, Dijkstra, ймовірно, сказав би: "Використання [PHP] калічить розум; тому його вчення слід розглядати як злочин".
Росс Паттерсон

12
@Ross. "зарозумілість в інформатиці вимірюється в нано-Дейкстрасі" - Алан Кей

33

1 - Ігноруйте помилку та отримайте 0результат. Реєстрація попередження, якщо можливо.

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

2 - Додайте NaNяк можливе значення чисел, але це ставить питання про те, як обробляти NaNзначення в інших областях мови.

Вам слід обробляти NaN так, як це виконується в інших мовах: будь-який подальший розрахунок також дає NaN, і кожне порівняння (навіть NaN == NaN) дає хибне значення.

Я вважаю, що це прийнятно, але не обов’язково нове дружнє товариство.

3 - Припинити виконання програми та повідомити користувачеві про сильну помилку.

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

Є також четвертий варіант. Зробіть поділ потрійною операцією. Будь-який із цих двох буде працювати:

  • div (чисельник, чисельник, альтернативний_результат)
  • div (чисельник, чисельник, альтернативний_зчисник)

Але якщо ви зробите NaN == NaNце false, вам доведеться додати isNaN()функцію, щоб користувачі могли виявити NaNs.
AJMansfield

2
@AJMansfield: Чи це, чи люди , реалізувати його самі: isNan(x) => x != x. Все-таки, коли ви NaNз’являєтесь у своєму програмному коді, вам не слід починати додавати isNaNчеки, а скоріше відстежувати причину та робити там необхідні перевірки. Тому важливо NaNповноцінно розмножуватися.
back2dos

5
NaNs в основному контрінтуїтивні. Мовою для початківців вони мертві по приїзді.
Росс Паттерсон

2
@RossPatterson Але початківець може легко сказати 1/0- ви повинні щось зробити з цим. Немає корисного результату, окрім Infабо NaN- чогось, що поширюватиме помилку далі в програмі. Інакше єдине рішення - зупинитись із помилкою на цьому етапі.
Марк Херд

1
Варіант 4 можна вдосконалити, дозволяючи викликати функцію, яка в свою чергу може виконувати будь-які дії, необхідні для відновлення з несподіваного дільника 0.
CyberFonic

21

Закрийте запущену програму з надзвичайними упередженнями. (Надаючи адекватну інформацію про налагодження)

Потім навчіть своїх користувачів визначати та обробляти умови, де дільник може бути нульовим (введені користувачем значення тощо)


13

У Haskell (і аналогічно в Scala), замість того , щоб кидати виключення (або повертати порожні посилання) типи обгортки Maybeі Eitherможуть бути використані. У Maybeкористувача є можливість перевірити, чи отримане ним значення "порожнє", або він може надати значення за замовчуванням при "розпакуванні". Eitherподібний, але може бути використаний повертає об'єкт (наприклад, рядок помилки), що описує проблему, якщо така є.


1
Щоправда, але зауважте, що Haskell не використовує це для ділення на нуль. Натомість кожен тип Haskell неявно має "дно" як можливе значення. Це не схоже на нульові покажчики в тому сенсі, що це "значення" виразу, яке не вдається закінчити. Ви, звичайно, не можете перевірити на невиразність як значення, але в оперативній семантиці випадки, які не вдається закінчити, є частиною значення виразу. У Haskell це "нижнє" значення також обробляє додаткові результати помилок, такі як error "some message"функція, що оцінюється.
Steve314

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

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

Я думаю, що це шлях. ... Мова програмування Rust широко використовує його у своїй стандартній бібліотеці.
aochagavia

12

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

x = ...
y = ...

if y ≠ 0:
  return x / y    // In this block, y is known to be nonzero.
else:
  return x / y    // This, however, is a compile-time error.

Крім того, мати інтелектуальну функцію затвердження, яка встановлює інваріанти:

x = ...
require x ≠ 0, "Unexpected zero in calculation"
// For the remainder of this scope, x is known to be nonzero.

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

x = ...           // env1 = { x :: int }
y = ...           // env2 = env1 + { y :: int }
if y ≠ 0:         // env3 = env2 + { y ≠ 0 }
  return x / y    // (/) :: (int, int ≠ 0) → int
else:             // env4 = env2 + { y = 0 }
  ...
...               // env5 = env2

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


4
Ідеальна ідея, але цей спосіб вирішення обмежень не є повним. Уявіть щось подібне def foo(a,b): return a / ord(sha1(b)[0]). Статичний аналізатор не може інвертувати SHA-1. Кланг має такий тип статичного аналізу, і він відмінно підходить для пошуку дрібних помилок, але є маса випадків, з якими він не може впоратися.
Марк Е. Хааз

9
це не є NP-завершеним, це неможливо - говорить лемінг зупинки. Однак статичному аналізатору не потрібно цього вирішувати, він може просто вивернутись на таке твердження і вимагати від вас додати явне твердження чи декор.
MK01

1
@ MK01: Іншими словами, аналіз "консервативний".
Джон Перді

11

Число 1 (вставити незаперечний нуль) завжди погано. Вибір між №2 (розповсюдження NaN) та №3 (вбивство процесу) залежить від контексту і в ідеалі має бути глобальним налаштуванням, як це є в Numpy.

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

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

Інший варіант, пов'язаний з NaNs: та сама специфікація IEEE з плаваючою комою визначає Inf та -Inf, і вони поширюються інакше, ніж NaN. Наприклад, я майже впевнений, що Inf> будь-яке число та -Inf <будь-яке число, що було б те, що ви хотіли, якби поділ на нуль стався, тому що нуль повинен був бути лише невеликим числом. Якщо ваші входи округлені і зазнають помилок вимірювання (наприклад, фізичні вимірювання, зроблені вручну), різниця двох великих величин може призвести до нуля. Без поділу на нуль ви отримали б велику кількість, і, можливо, вам не байдуже, яка вона велика. У цьому випадку In і -Inf - це досконало достовірні результати.

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


Але ми не можемо сказати, чи повинен був знаменник бути позитивним чи негативним, тож поділ може давати + inf, коли -inf було бажано, або візово.
Даніель Любаров

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

Якщо ви працюєте в такій системі, вам доведеться ідентифікувати + inf і -inf як еквівалентні так само, як ви повинні ідентифікувати +0 і -0 як еквівалентні, навіть якщо вони мають різні двійкові уявлення.
Джим Піварський

8

3. Припиніть виконання програми та повідомте користувачеві про сильну помилку.

[Цей параметр] не практичний ...

Звичайно, це практично: обов'язок програмістів написати програму, яка насправді має сенс. Ділення на 0 не має сенсу. Тому, якщо програміст виконує підрозділ, він також зобов'язаний заздалегідь переконатися, що дільник не дорівнює 0. Якщо програміст не виконає цю перевірку перевірки, тоді він повинен зрозуміти цю помилку, як тільки можливо, і денормалізовані (NaN) або неправильні (0) результати обчислення просто не допоможуть у цьому відношенні.

Варіант 3 виявляється таким, який я б вам рекомендував, btw, за той, що є найпростішим, чесним та математично правильним.


4

Мені здається поганою ідеєю виконувати важливі завдання (тобто; «нічний крон») в середовищі, де помилки ігноруються. Страшна ідея зробити цю особливість. Це виключає варіанти 1 і 2.

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


3

IEEE 754 насправді має чітко визначене рішення вашої проблеми. Обробка винятків без використання exceptions http://en.wikipedia.org/wiki/IEEE_floating_point#Exception_handling

1/0  = Inf
-1/0 = -Inf
0/0  = NaN

таким чином всі ваші операції мають математичний сенс.

\ lim_ {x \ to 0} 1 / x = Інф

На мою думку, наступний IEEE 754 має найбільш сенс, оскільки він гарантує, що ваші обчислення є такими ж правильними, як і на комп'ютері, а також ви відповідаєте поведінці інших мов програмування.

Єдина проблема, яка виникає, полягає в тому, що Inf та NaN забруднить ваші результати, і ваші користувачі не будуть точно знати, звідки ця проблема. Погляньте на мову на зразок Джулії, яка робить це досить добре.

julia> 1/0
Inf

julia> -1/0
-Inf

julia> 0/0
NaN

julia> a = [1,1,1] ./ [2,1,0]
3-element Array{Float64,1}:
   0.5
   1.0
 Inf

julia> sum(a)
Inf

julia> a = [1,1,0] ./ [2,1,0]
3-element Array{Float64,1}:
   0.5
   1.0
 NaN

julia> sum(a)
NaN

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

edit:Я не побачив другої частини відповіді Джима Піварського, що в основному те, що я говорю вище. Моє ліжко.


2

SQL, що легко використовується мовою, яка найбільш широко використовується непрограмістами, робить №3, за всім вартім. З мого досвіду спостереження та надання допомоги непрограмістам у написанні SQL така поведінка, як правило, добре зрозуміла і легко компенсується (з викладом справи тощо). Це допомагає, що повідомлення про помилку, яке ви отримуєте, є досить прямим, наприклад, у Postgres 9 ви отримуєте "ПОМИЛКА: поділ на нуль".


2

Я думаю, що проблема "орієнтована на початківців користувачів. -> Отже, немає підтримки для ..."

Чому ви вважаєте, що обробка винятків проблематична для початківців користувачів?

Що гірше? Маєте "складну" особливість або не маєте поняття, чому щось сталося? Що може заплутати більше? Збій з дампсом ядра або "Фатальна помилка: розділити на нуль"?

Натомість, я думаю, що ДАЛЬШЕ краще орієнтуватися на ВЕЛИКІ помилки повідомлення. Тож зробіть замість цього: "Неправильний розрахунок, розділіть 0/0" (тобто: Завжди показуйте ДАНІ, що спричиняють проблему, а не лише вид проблеми). Подивіться, як PostgreSql роблять помилки з повідомленнями, які є чудовими IMHO.

Однак ви можете переглянути інші способи роботи з винятками, як-от:

http://dlang.org/exception-safe.html

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

def openFile(fileName): File | Exception
    if not(File.Exist(fileName)):
        raise FileNotExist(fileName)
    else:
        return File.Open()

#This cause a exception:

theFile = openFile('not exist')

# But this, not:

theFile | err = openFile('not exist')

1

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

Імовірні дії включають: a) завершення (b) спонукання користувача до дії (c) реєстрація помилки (d) заміна виправленого значення (e) встановлення індикатора, який підлягає тестуванню в коді (f), викликає порядок обробки помилок. Які з них ви робите доступними та якими способами є вибір, який ви повинні зробити.

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

[Для прикладу розглянемо таблицю Excel. Excel не скасовує вашу електронну таблицю, оскільки кількість переповнюється чи інше. Клітина отримує дивне значення, і ви дізнаєтеся, чому і виправити це.]

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

Розкриття: Я створив саме таку мовну реалізацію (Powerflex) і вирішив саме цю проблему (та багато інших) у 1980-х. Протягом останніх 20 років у мовах спостерігається незначний або зовсім незначний прогрес у мовах, і ви будете залучати купу критики за спробу, але я дуже сподіваюся, що вам це вдасться.


1

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

Ще одна ідея, яку я не бачив, - це створити загальне "недійсне" значення. Загальне "ця змінна не має значення, оскільки програма зробила щось погане", яке несе повний слід стека. Тоді, якщо ви коли-небудь використовувати це значення де-небудь, результат знову буде недійсним, при цьому нова операція намагається зверху (тобто, якщо невірне значення коли-небудь з'являється в виразі, весь вираз призводить до недійсного, і жодні виклики функцій не намагаються; виняток буде бути булевими операторами - правда чи невірна правда, а неправда - неправда, - неправда - можуть бути й інші винятки). Як тільки це значення більше не посилається нікуди, ви записуєте хороший довгий опис усієї ланцюга, де все було не так, і продовжуєте дію як завжди. Можливо, надішліть електронний лист до ведучого проекту чи чогось іншого.

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


1

Є дві основні причини поділу на нуль.

  1. У точній моделі (на зразок цілих чисел) ви отримуєте ділення на нуль DBZ, оскільки введення неправильне. Це такий тип ДБЗ, про який думає більшість із нас.
  2. У неточній моделі (на зразок плаваючої pt) ви можете отримати DBZ через помилку округлення, навіть якщо введення є дійсним. Це те, про що ми зазвичай не думаємо.

Для 1. ви повинні повідомити користувачам про те, що вони допустили помилку, оскільки вони несуть відповідальність, і вони найкраще знають, як виправити ситуацію.

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

Я бачу особу, яка задає це запитання, запитуючи на випадок 1. Тому вам потрібно повідомити користувача. Використовуючи будь-який стандарт з плаваючою комою, Inf, -Inf, Nan, IEEE не вписується в цю ситуацію. Принципово неправильна стратегія.


0

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

int div = random(0,100);
int b = 10000 / div; // Error E0000: div might be zero

Для цього вам потрібен новий числовий тип, натуральне число, на відміну від цілого числа. Це може бути ... важко ... впоратися.
Сервіс

@ Серві: Ні, ти б не зробив. Чому б ти? Вам потрібна логіка в компіляторі для визначення можливих значень, але ви все одно хочете (з оптимізації причин).
MSalters

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

@Servy: Ви помиляєтеся: компілятор може відстежувати цей стан, не потребуючи такого типу, і, наприклад, GCC вже це робить. Наприклад, тип C intдозволяє нульовим значенням, але GCC все ще може визначити, де в коді конкретні вставки не можуть бути нульовими.
MSalters

2
Але лише в певних випадках; не може це зробити зі 100% точністю у всіх випадках. У вас будуть або помилкові позитиви, або помилкові негативи. Це вірно правда. Наприклад, я можу створити фрагмент коду, який може бути, а може і не закінчуватися . Якщо компілятор навіть не може знати, чи закінчився він, то як він міг дізнатися, чи є результат отриманого int не нульовим? Він може спіймати прості очевидні випадки, але не у всіх випадках.
Сервіс

0

Коли ви пишете мову програмування, ви повинні скористатися цим фактом і обов'язково включити дію для приладу за нульовим станом. a <= n / c: 0 ділення за нулем

Я знаю, що я тільки запропонував - це по суті додавання "goto" до вашого PL.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.