Как обрабатывать деление на ноль в языке, который не поддерживает исключения?


62

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

Я дошел до того, что мне нужно реализовать оператор деления, и мне интересно, как лучше всего обработать ошибку деления на ноль?

Кажется, у меня есть только три возможных способа справиться с этим делом.

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

Вариант № 1 кажется единственным разумным решением. Вариант № 3 не практичен, так как этот язык будет использоваться для запуска логики как ночной хрон.

Каковы мои альтернативы обработке ошибки деления на ноль, и каковы риски при использовании опции # 1.


12
если бы вы добавили поддержку исключений, а пользователь ее не перехватил, то у вас был бы вариант № 3
трещотка,

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

16
@delnan большинство новых языков созданы, чтобы позволить бизнес-правилам быть отделенными от того, как они реализованы. Пользователю не нужно знать, как это reject "Foo"было реализовано, а просто отклонить документ, если он содержит ключевое слово Foo. Я стараюсь сделать язык как можно более легким для чтения, используя термины, с которыми знаком пользователь. Предоставление пользователю их собственного языка программирования дает им возможность добавлять бизнес-правила, не завися от технического персонала.
Reactgular

19
@ Мэтью Фоскарини. Никогда, никогда не игнорируйте ошибку и молча возвращайте 0. При выполнении деления 0 может быть вполне допустимым значением (по какой-то причине в Power Basic есть такая вещь, и это действительно боль). Если вы поделили числа с плавающей запятой, Nan или Inf были бы хорошими (посмотрите на IEEE 754, чтобы понять почему). Если вы делите целые числа, вы можете остановить программу, деление на 0 никогда не должно быть разрешено (ну, если вы не хотите реализовать настоящую систему исключений).

16
Я удивлен и восхищен комплексом бизнес-областей, достаточно обоснованным, чтобы оправдать проприетарный, полный по Тьюрингу язык программирования, но достаточно слабым, чтобы терпеть радикально неточные результаты.
Марк Э. Хаас

Ответы:


98

Я настоятельно рекомендую против # 1, потому что игнорирование ошибок является опасным анти-паттерном. Это может привести к трудным для анализа ошибкам. Установка результата деления на ноль в 0 не имеет никакого смысла, и продолжение выполнения программы с бессмысленным значением вызовет проблемы. Особенно, когда программа работает без присмотра. Когда интерпретатор программы замечает, что в программе есть ошибка (а деление на ноль почти всегда является ошибкой проектирования), ее прерывание и сохранение всего как есть обычно предпочтительнее, чем заполнение вашей базы данных мусором.

Кроме того, вы вряд ли добьетесь успеха, если будете тщательно следовать этому шаблону. Рано или поздно вы столкнетесь с ошибочными ситуациями, которые просто нельзя игнорировать (например, из-за нехватки памяти или переполнения стека), и вам все равно придется реализовать способ завершения программы.

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

Между прочим: создание языка программирования, используемого непрограммистами, - это то, что мы пытаемся сделать с 1964 года (Dartmouth BASIC). До сих пор мы были неудачными. Но все равно удачи.


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

24
Да, это имеет. Когда я прочитал ваш вопрос, я сразу подумал, что это очень PHP-штука - создавать неправильный вывод и продолжать передвигаться перед лицом ошибок. Есть веские причины, по которым PHP является исключением в этом.
Джоэл

4
+1 за базовый комментарий. Я не советую использовать NaNязык начинающих, но в целом отличный ответ.
Росс Паттерсон

8
@Joel Если бы он прожил достаточно долго, Дейкстра, вероятно, сказал бы: «Использование [PHP] наносит вред разуму, поэтому его обучение следует рассматривать как уголовное преступление».
Росс Паттерсон

12
@Ross. «Высокомерие в информатике измеряется в нано-Дейкстре» - Алан Кей

33

1 - игнорировать ошибку и вывести 0результат. Регистрация предупреждения, если это возможно.

Это не очень хорошая идея. Вообще. Люди начнут зависеть от этого, и если вы когда-нибудь это исправите, вы сломаете много кода.

2 - Добавьте NaNв качестве возможного значения для чисел, но это вызывает вопросы о том, как обрабатывать NaNзначения в других областях языка.

Вы должны обрабатывать NaN так, как это делают среды выполнения других языков: любые дальнейшие вычисления также дают NaN, а каждое сравнение (даже NaN == NaN) дает ложь.

Я думаю, что это приемлемо, но не обязательно для новичков.

3 - прекратить выполнение программы и сообщить пользователю о серьезной ошибке.

Это лучшее решение, я думаю. Имея эту информацию в руках, пользователи должны иметь возможность обрабатывать 0. Вы должны предоставить среду тестирования, особенно если она предназначена для запуска раз в ночь.

Там также четвертый вариант. Сделать разделение троичной операцией. Любой из этих двух будет работать:

  • div (числитель, числитель, alternative_result)
  • div (числитель, числитель, alternative_denumerator)

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

2
@AJMansfield: Либо так, либо люди реализуют это сами isNan(x) => x != x. Тем не менее, когда вы NaNподходите к своему программному коду, вы не должны начинать добавлять isNaNпроверки, а скорее отслеживать причину и делать необходимые проверки там. Поэтому важно, NaNчтобы они размножались полностью.
back2dos

5
NaNв основном нелогичные. На языке начинающих, они мертвы по прибытии.
Росс Паттерсон

2
@ RossPatterson Но новичок может легко сказать 1/0- с этим нужно что-то делать. Не может быть полезного результата, кроме Infили NaN- что-то, что будет распространять ошибку дальше в программу. В противном случае единственным решением является остановка с ошибкой на этом этапе.
Марк Херд

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

21

Завершите работающее приложение с предубеждением. (Предоставляя адекватную отладочную информацию)

Затем обучите своих пользователей определять и обрабатывать условия, при которых делитель может быть равен нулю (введенные пользователем значения и т. Д.)


13

В Haskell (и аналогично в Scala), вместо того , чтобы бросать исключения (или возвращать пустые ссылки) типы обертки Maybeи Eitherмогут быть использованы. У Maybeпользователя есть возможность проверить, является ли полученное им значение «пустым», или он может предоставить значение по умолчанию при «разворачивании». Eitherаналогично, но может использоваться, возвращает объект (например, строку ошибки), описывающий проблему, если таковой имеется.


1
Верно, но обратите внимание, что Haskell не использует это для деления на ноль. Вместо этого каждый тип Haskell неявно имеет в качестве возможного значения «bottom». Это не похоже на нулевые указатели в том смысле, что это «значение» выражения, которое не завершается. Конечно, вы не можете проверить на предмет нетерминирования как значение, но в операционной семантике случаи, которые не заканчиваются, являются частью значения выражения. В Haskell это «нижнее» значение также обрабатывает дополнительные результаты в случае ошибки, такие как error "some message"оцениваемая функция.
Steve314

Лично, если эффект прерывания всей программы считается действительным, я не знаю, почему чистый код не может вызвать исключение, но это только я - Haskellне позволяет чистым выражениям генерировать исключения.
Steve314

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

Я думаю, что это путь ... Язык программирования Rust широко использует его в своей стандартной библиотеке.
аочагавия

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
Идеальная идея, но этот тип решения ограничений является NP-полным. Представь что-то вроде def foo(a,b): return a / ord(sha1(b)[0]). Статический анализатор не может инвертировать SHA-1. Clang имеет такой тип статического анализа и отлично подходит для поиска мелких ошибок, но есть множество случаев, с которыми он не может справиться.
Марк Э. Хаас

9
это не NP-полная, это невозможно, - говорит остановка леммы. Тем не менее, статический анализатор не должен решать эту проблему, он может просто наплевать на подобное утверждение и потребовать от вас добавить явное утверждение или декорацию.
MK01

1
@ MK01: Другими словами, анализ «консервативный».
Джон Перди

11

Число 1 (вставьте неустранимый ноль) всегда плохо. Выбор между # 2 (распространять NaN) и # 3 (убить процесс) зависит от контекста и в идеале должен быть глобальным, как в Numpy.

Если вы делаете одно большое интегрированное вычисление, распространение NaN - плохая идея, потому что в конечном итоге оно распространится и заразит весь ваш расчет - когда вы посмотрите на результаты утром и увидите, что все они являются NaN, вы Я должен выбросить результаты и начать все равно заново. Было бы лучше, если бы программа завершилась, вы получили звонок среди ночи и исправили его - по крайней мере, с точки зрения количества потраченных часов.

Если вы выполняете много небольших, в основном независимых вычислений (таких как сокращение карты или смущающие параллельные вычисления), и вы можете допустить, что некоторый процент из них будет непригодным для использования из-за NaN, это, вероятно, лучший вариант. Завершение программы и невыполнение 99%, которые были бы хорошими и полезными из-за 1%, которые искажены и делятся на ноль, могут быть ошибкой.

Другая опция, связанная с NaN: та же спецификация IEEE с плавающей точкой определяет Inf и -Inf, и они распространяются не так, как NaN. Например, я уверен, что Inf> любое число и -Inf <любое число, что было бы тем, что вы хотели, если бы ваше деление на ноль произошло, потому что ноль просто должен был быть небольшим числом. Если ваши входы округлены и имеют ошибку измерения (например, физические измерения, сделанные вручную), разница двух больших величин может привести к нулю. Без деления на ноль вы бы получили большое число, и, возможно, вам все равно, насколько оно велико. В этом случае In и -Inf - совершенно правильные результаты.

Это также может быть формально правильно - просто скажем, что вы работаете в расширенных реалах.


Но мы не можем сказать, был ли знаменатель задуман как положительный или отрицательный, поэтому деление может дать + inf, когда -inf был желателен, или наоборот.
Даниэль Любаров

Правда, ваша ошибка измерения слишком мала, чтобы различать + inf и -inf. Это наиболее близко напоминает сферу Римана, в которой вся комплексная плоскость отображается на шар с ровно одной бесконечной точкой (точка, диаметрально противоположная началу координат). Очень большие положительные числа, очень большие отрицательные числа и даже очень большие мнимые и комплексные числа - все это близко к этой бесконечной точке. С небольшой погрешностью измерения вы не сможете их различить.
Джим Пиварски

Если вы работаете в такой системе, вам придется идентифицировать + inf и -inf как эквивалентные, так же как вы должны идентифицировать +0 и -0 как эквивалентные, даже если они имеют разные двоичные представления.
Джим Пиварски

8

3. Завершите выполнение программы и сообщите пользователю о серьезной ошибке.

[Этот вариант] не практичен ...

Конечно, это практично: программисты обязаны писать программы, которые действительно имеют смысл. Деление на 0 не имеет никакого смысла. Поэтому, если программист выполняет деление, он также обязан заранее проверить, что делитель не равен 0. Если программист не может выполнить эту проверку достоверности, то он должен осознать эту ошибку, как только возможные и ненормализованные (NaN) или неправильные (0) результаты вычислений просто не помогут в этом отношении.

Вариант 3 оказался тем, который я бы порекомендовал вам, между прочим, за то, что он самый простой, честный и математически правильный.


4

Мне кажется плохой идеей запускать важные задачи (например, «ночной cron») в среде, где ошибки игнорируются. Это ужасная идея, чтобы сделать это особенность. Это исключает варианты 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 = Inf

На мой взгляд, лучше всего придерживаться стандарта 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, это поведение, как правило, хорошо понимается и легко компенсируется (с помощью оператора case или тому подобного). Помогает то, что полученное вами сообщение об ошибке имеет тенденцию быть довольно прямым, например, в Postgres 9 вы получите «ОШИБКА: деление на ноль».


2

Я думаю, что проблема "ориентирована на начинающих пользователей. -> Так что нет поддержки ..."

Почему вы думаете, что обработка исключений проблематична для начинающих пользователей?

Что хуже? У вас есть «сложная» функция или нет понятия, почему что-то случилось? Что может смутить больше? Сбой с дампом ядра или «Неустранимая ошибка: делить на ноль»?

Вместо этого, я думаю, что FAR лучше стремиться к БОЛЬШИМ ошибкам в сообщениях. Вместо этого сделайте следующее: «Плохой расчет, разделите 0/0» (то есть: всегда показывайте ДАННЫЕ, которые вызывают проблему, а не только вид проблемы). Посмотрите, как PostgreSql делает сообщения об ошибках, это здорово, ИМХО.

Однако вы можете посмотреть на другие способы работы с исключениями, такими как:

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, потому что ввод неправильный. Это тот тип DBZ, о котором думает большинство из нас.
  2. В неточной модели (например, с плавающей точкой) вы можете получить DBZ из-за ошибки округления, даже если введенные данные верны. Это то, о чем мы обычно не думаем.

Для 1. Вы должны сообщить пользователям, что они допустили ошибку, потому что именно они несут ответственность, и они лучше всех знают, как исправить ситуацию.

Для 2. Это не ошибка пользователя, вы можете указать на алгоритм, аппаратную реализацию и т. Д., Но это не ошибка пользователя, поэтому вы не должны завершать программу или даже генерировать исключение (если это разрешено, что не в данном случае). Таким образом, разумное решение состоит в том, чтобы продолжить операции каким-либо разумным способом.

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


0

Запретить это на языке. То есть запрещать деление на число до тех пор, пока оно не станет доказуемо ненулевым, обычно сначала проверяя его. То есть.

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

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

@Servy: нет, вы бы не. Почему ты? Вам нужна логика в компиляторе для определения возможных значений, но вы все равно это хотите (по причинам оптимизации).
MSalters

Если у вас нет другого типа, один для нуля и один для ненулевых значений, то вы не сможете решить проблему в общем случае. У вас либо будут ложные срабатывания, и вы заставляете пользователя проверять нулевой путь чаще, чем он должен на самом деле, либо вы создадите ситуации, когда они все еще могут делиться на ноль.
Serv

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

2
Но только в определенных случаях; во всех случаях это невозможно сделать со 100% точностью. У вас будут либо ложные срабатывания, либо ложные отрицания. Это доказуемо верно. Например, я мог бы создать фрагмент кода, который может или не может быть завершен . Если компилятор даже не может знать, завершился ли он, как он мог узнать, что итоговое значение int не равно нулю? Это может поймать простые очевидные случаи, но не все случаи.
Serv

0

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

Я знаю, что я только что предложил, по сути, добавить «goto» к вашему PL.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.