如何在不支持异常的语言中处理零除?


62

我正在开发一种新的编程语言来解决一些业务需求,并且该语言面向新手用户。因此,不支持该语言中的异常处理,即使我添加了它,我也不希望他们使用它。

我已经到了必须实现除法运算符的地步,我想知道如何最好地处理除零错误?

我似乎只有三种可能的方式来处理这种情况。

  1. 忽略错误并产生0结果。尽可能记录警告。
  2. 将其添加NaN为数字的可能值,但这引发了有关如何处理NaN语言其他区域中的值的问题。
  3. 终止程序的执行并将严重错误报告给用户。

选项1似乎是唯一合理的解决方案。选项#3不切实际,因为该语言将用作夜间cron来运行逻辑。

解决零除错误的方法是什么,选择#1会有什么风险。


12
如果您确实添加了异常支持并且用户没有捕获到异常支持,那么您将拥有选项3
棘手怪胎

82
我很好奇,什么样的愚蠢要求会要求您创建一种全新的编程语言?根据我的经验,制作的每个语言很烂(设计或执行中,往往在两者)并不合理了很大的努力,甚至得到的是很多。第一个例外,但第二个例外,因为它们很容易<0.01%,所以很可能是测量错误;-)

16
@delnan创建了大多数新语言,以使业务规则与实现方式分开。用户不需要知道如何reject "Foo"实现,而只要文档包含关键字,就可以拒绝文档Foo。我尝试使用用户熟悉的术语使该语言易于阅读。为用户提供他们自己的编程语言,使他们无需依赖技术人员即可添加业务规则。
Reactgular

19
@Mathew Foscarini。永远不要忽略该错误并默默地返回0。进行除法时,0可能是一个完全合法的值(由于某种原因,Power Basic中存在这种情况,这确实很痛苦)。如果忽略浮点数,则Nan或Inf会很好(请查看IEEE 754了解原因)。如果对整数进行除法,则可以停止程序,永远不允许除以0(好吧,除非要实现真正的异常系统)。

16
我对一个足以证明一种专有的,图灵完备的编程语言的商业领域很感兴趣,但又松懈到足以容忍非常不准确的结果。
Mark E. Haase 2014年

Answers:


98

我强烈建议不要使用#1,因为仅忽略错误是一种危险的反模式。它可能导致难以分析的错误。将除以零的结果设置为0毫无意义,并且继续以无意义的值执行程序会造成麻烦。特别是在程序无人值守运行时。当程序解释器注意到程序中有错误时(并且零除几乎总是设计错误),通常比将垃圾填满数据库更喜欢中止它并保持一切不变。

另外,彻底遵循此模式将不太可能成功。迟早您将遇到无法忽略的错误情况(例如内存不足或堆栈溢出),并且无论如何您都必须实现一种终止程序的方法。

选项#2(使用NaN)会有点工作,但没有您想象的那么多。IEEE 754标准已详细说明了如何在不同的计算中处理NaN,因此您很可能可以执行解释器所使用的语言。

顺便说一句:自1964年以来,我们一直在尝试创建一种可供非程序员使用的编程语言(达特茅斯BASIC)。到目前为止,我们一直没有成功。但是无论如何,祝你好运。


14
+1谢谢。您说服我抛出了一个错误,现在我读了您的回答,我不明白为什么我会犹豫。PHP对我影响很大。
Reactgular

24
是的,它有。当我读到您的问题时,我立即想到产生错误的输出并面对错误继续前进是非常PHP风格的事情。有充分的理由说明为什么PHP是例外。
2013年

4
+1表示BASIC注释。我不建议使用NaN初学者的语言,但总的来说,这是一个很好的答案。
罗斯·帕特森

8
@Joel如果他活了足够长的时间,Dijkstra可能会说:“使用[PHP]会使思想瘫痪;因此,应将其教its视为刑事犯罪。”
罗斯·帕特森

12
罗斯 “计算机科学中的傲慢程度是以nano-Dijkstras来衡量的” –艾伦·凯

33

1-忽略错误并产生0结果。尽可能记录警告。

那不是一个好主意。完全没有 人们将开始依赖它,并且如果您要对其进行修复,则会破坏很多代码。

2-将NaN数字添加为可能的值,但这引发了有关如何处理NaN语言其他区域中的值的问题。

您应该按照其他语言的运行时的方式来处理NaN:任何进一步的计算也会产生NaN,而每次比较(甚至是NaN == NaN)都会得出false。

我认为这是可以接受的,但不一定对新手友好。

3-终止程序执行,并向用户报告严重错误。

我认为这是最好的解决方案。有了这些信息,用户应该能够处理0。您应该提供一个测试环境,尤其是如果打算每晚运行一次。

还有第四种选择。使除法成为三元运算。这两个都可以使用:

  • div(分子,分子,替代结果)
  • div(分子,分母,alternate_denumerator)

但是,如果您将NaN == NaNbe设置为false,则必须添加一个isNaN()函数,以便用户能够检测到NaN
AJMansfield

2
@AJMansfield:要么,要么人们自己实现:isNan(x) => x != x。尽管如此,当您NaN进入编程代码时,您不应开始添加isNaN检查,而应在其中查找原因并进行必要的检查。因此,NaN充分传播非常重要。
back2dos

5
NaNs主要是违反直觉的。用初学者的语言,他们在抵达时就死了。
罗斯·帕特森

2
@RossPatterson但是,初学者可以轻松地说1/0-您必须对此做些事情。除Inf或以外,没有其他有用的结果NaN,可能会使错误进一步传播到程序中。否则,唯一的解决方案是此时停止并出现错误。
马克·赫德

1
可以通过允许调用一个函数来改进选项4,该函数又可以执行从意外0除数恢复所需的任何操作。
Cyber​​Fonic

21

以极端的偏见终止正在运行的应用程序。(尽管提供了足够的调试信息)

然后教育您的用户以识别和处理除数可能为零的条件(用户输入的值等)。


13

在Haskell(和Scala中类似),而不是抛出异常(或返回空引用)的包装类型MaybeEither可以使用。随着Maybe用户有机会来测试,如果他得到的值是“空”,或者他可能会提供当“展开”的默认值。Either类似,但是可以使用它返回一个描述问题的对象(例如,错误字符串)(如果存在)。


1
是的,但是请注意,Haskell不会将其用于零除。而是,每个Haskell类型都隐式地将“底部”作为可能的值。从某种意义上说,它是无法终止的表达式的“值”,这与空指针不同。当然,您不能将非终止作为值进行测试,但是在操作语义中,无法终止的情况是表达式含义的一部分。在Haskell中,该“底部”值还处理其他错误情况结果,例如error "some message"正在评估的函数。
Steve314

就个人而言,如果中止整个程序的效果被认为是有效的,那么我不知道为什么纯代码不能具有引发异常的效果,但这只是我- Haskell不允许纯表达式引发异常。
Steve314

我认为这是个好主意,因为除了抛出异常外,所有建议的选项都不会向用户传达他们犯了错误的信息。基本思想是用户会错给他们给程序的值,因此程序应告诉用户他们输入了错误的内容(然后用户可以想到一种补救方法)。没有告诉用户他们的错误,任何解决方案都感觉很奇怪。
通知A

我认为这是要走的路。。。Rust编程语言在其标准库中广泛使用它。
aochagavia 2014年

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具有这种类型的静态分析,非常适合发现浅层错误,但很多情况下它无法处理。
Mark E. Haase 2014年

9
这不是NP完全的,这是不可能的-停止引理。但是,静态分析器不需要解决此问题,它可以处理类似这样的语句,并要求您添加显式的断言或修饰。
MK01 2014年

1
@ MK01:换句话说,分析是“保守的”。
乔恩·普迪

11

数字1(插入不可容忍的零)总是不好的。#2(传播NaN)和#3(杀死进程)之间的选择取决于上下文,并且理想情况下应该是全局设置,就像在Numpy中一样。

如果您要进行大型的综合计算,那么传播NaN并不是一个好主意,因为它最终会传播并感染您的整个计算---当您早上查看结果并发现它们都是NaN时, d必须抛出结果并重新开始。如果程序终止,最好是在半夜接到电话并修复它-至少要减少浪费的时间,这样会更好。

如果您正在执行许多很少有的,几乎独立的计算(例如map-reduce或令人尴尬的并行计算),并且您可以容忍其中某些百分比由于NaN而无法使用,那可能是最好的选择。由于格式错误并除以零的1%,终止该程序而不执行99%的有益和有用的操作可能是一个错误。

与NaN有关的另一种选择:相同的IEEE浮点规范定义了Inf和-Inf,它们的传播方式不同于NaN。例如,我非常确定Inf>任何数字和-Inf <任何数字,如果除以零的情况发生的话,这就是您想要的,因为零应该只是一个小数字。如果您的输入是四舍五入的并且遭受测量误差(例如手工进行的物理测量),则两个较大量的差可能导致零。没有零除,您将得到一些大数,也许您并不关心它的大小。在这种情况下,In和-Inf是完全有效的结果。

它在形式上也可能是正确的-只是说您在扩展实数中工作。


但是我们无法确定分母是正数还是负数,因此当需要-inf时,除法可能会产生+ inf,反之亦然。
Daniel Lubarov 2013年

没错,您的测量误差太小,无法区分+ inf和-inf。这与里曼球面最相似,在里曼球面中,整个复杂平面都映射到一个球,球上恰好有一个无限点(与原点直径相反的点)。非常大的正数,非常大的负数,甚至非常大的虚数和复数都接近该无限点。仅有少量测量误差,您无法区分它们。
Jim Pivarski 2013年

如果在这种系统上工作,则必须将+ inf和-inf标识为等效,就像必须将+0和-0标识为等效,即使它们具有不同的二进制表示形式也是如此。
吉姆·皮瓦尔斯基

8

3.终止程序的执行,并向用户报告发生严重错误。

[此选项]不可行...

当然,这是实用的:编写真正有意义的程序是程序员的责任。除以0毫无意义。因此,如果程序员正在执行除法,则他/她还有责任事先验证除数是否等于0。如果程序员未能执行该验证检查,则他/她应尽快意识到该错误。可能的,并且归一化(NaN)或不正确(0)的计算结果在这方面根本无济于事。

选项3恰好是我会推荐给您的选项,顺便说一句,因为它是最直接,最诚实和数学上正确的选项。


4

对我来说,在忽略错误的环境中运行重要任务(例如“每晚cron”)对我来说似乎是一个坏主意。使此功能成为一个好主意。这排除了选项1和2。

选项3是唯一可接受的解决方案。例外不一定是语言的一部分,但它们是现实的一部分。您的终止消息应尽可能具体且信息丰富。


3

IEEE 754实际上为您的问题提供了明确定义的解决方案。不使用http://en.wikipedia.org/wiki/IEEE_floating_point#Exception_handling进行异常处理exceptions

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

这样,您的所有操作在数学上都是有意义的。

\ lim_ {x \ to 0} 1 / x = Inf

在我看来,遵循IEEE 754是最有意义的,因为它可以确保您的计算与在计算机上一样正确,并且还与其他编程语言的行为保持一致。

出现的唯一问题是Inf和NaN会污染您的结果,您的用户将无法确切知道问题的根源。看一看Julia这样的语言,它做得很好。

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:我没有看到吉姆·皮瓦尔斯基(Jim Pivarski)回答的第二部分,这基本上就是我在上面所说的。我的错。


2

SQL,很容易被非程序员广泛使用的语言,它会以其价值而排名第三。根据我观察和协助非程序员编写SQL的经验,通常会很好地理解此行为,并且很容易就可以补偿(使用case语句等)。这有助于您获得的错误消息趋向于非常直接,例如在Postgres 9中,您会收到“错误:被零除”。


2

我认为问题是“针对新手。->因此不支持...”

您为什么认为异常处理对新手用户有问题?

有什么更糟的?具有“困难”功能或不知道为什么会发生什么?还有什么会引起混淆?发生核心转储崩溃或“致命错误:被零除”?

相反,我认为FAR更好地针对GREAT消息错误。而是这样做:“错误的计算,除以0/0”(即:始终显示引起问题的数据,而不仅仅是问题的类型)。看一下PostgreSql如何处理消息错误,这是很棒的恕我直言。

但是,您可以查看其他处理异常的方法,例如:

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

我也梦想构建一种语言,在这种情况下,我认为将Maybe / Optional与常规Exception混合在一起可能是最好的:

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,您可以在其中提供备用值。

我没有看到的另一个想法是产生一个通用的“无效”值。一般情况下,“此变量没有值,因为程序做错了事情”,它本身带有完整的堆栈跟踪。然后,如果您在任何地方使用该值,则结果将再次无效,并尝试新操作(例如,如果无效值曾经出现在表达式中,则整个表达式将产生无效且不尝试任何函数调用;否则将产生异常)是布尔运算符-true或invalid为true,false和invalid为false-可能还有其他例外)。一旦不再在任何地方引用该值,您就会在整个链上记录一个很长的描述,以描述出现问题的地方,并照常营业。也许通过电子邮件将跟踪结果发送给项目负责人或其他人。

基本上像Maybe monad之类的东西。它也可以与其他任何可能失败的东西一起使用,并且您可以允许人们构造自己的无效对象。只要错误不太严重,程序就将继续运行,我认为这是这里真正想要的。


1

除以零有两个根本原因。

  1. 在精确模型(如整数)中,由于输入错误,您将得到除以零的DBZ。我们大多数人都想到的就是这种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

为此,您需要一个新的数字类型,即自然数,而不是整数。那可能……难以应对。
Servy

@Servy:不,你不会。你怎么会 您确实需要编译器中的逻辑来确定可能的值,但是无论如何都希望这样做(出于优化的原因)。
MSalters 2013年

如果没有其他类型,一个代表零,一个代表非零,那么一般情况下您将无法解决问题。您可能会产生误报,并迫使用户比实际情况更频繁地对零进行检查,或者您会创建仍然可以被零除的情况。
Servy

@Servy:您误会了:编译器无需这种类型就可以跟踪该状态,例如GCC已经这样做了。例如,C类型int允许零值,但是GCC仍可以确定代码中特定int不能为零的位置。
MSalters 2013年

2
但仅在某些情况下;在所有情况下,它都无法做到100%准确。您可能会有误报或误报。事实证明这是正确的。例如,我可以创建一个代码片段,它可能会或可能不会完成。如果编译器甚至不知道它是否完成了,怎么知道结果int是否为非零?它可以捕获简单的明显情况,但不能捕获所有情况。
Servy

0

在编写编程语言时,您应该利用这一事实,并使其成为强制性的,以包括零状态设计的动作。a <= n / c:0零动作

我知道我刚刚建议的实质上是在PL中添加“ goto”。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.