像Elm所说的那样,拥有“没有运行时异常”有什么好处?


16

某些语言声称具有“无运行时异常”,这是与具有它们的其他语言相比的明显优势。

我对此事感到困惑。

据我所知,运行时异常只是一种工具,使用得当:

  • 您可以传达“脏”状态(抛出意外数据)
  • 添加堆栈,您可以指向错误链
  • 您可以区分混乱(例如,在无效输入上返回空值)和需要开发人员注意的不安全用法(例如,在无效输入上引发异常)
  • 您可以通过异常消息为错误添加详细信息,从而提供进一步有用的详细信息,以帮助调试(理论上)

另一方面,我发现很难调试“吞噬”异常的软件。例如

try { 
  myFailingCode(); 
} catch {
  // no logs, no crashes, just a dirty state
}

因此,问题是:拥有“无运行时异常”的强大的理论优势是什么?


https://guide.elm-lang.org/

实际上没有运行时错误。没有空值。没有未定义不是函数。


我认为提供一个或两个您所引用的语言示例和/或指向此类声明的链接会有所帮助。这可以用多种方式来解释。
JimmyJames '16

我发现最近的一个例子是榆树相比,C,C ++,C#,Java和ECMAScript中,等等。我已经更新了我的问题@JimmyJames
atoth

2
这是我第一次听说。我的第一反应是打电话给BS。注意黄鼠狼的话:在实践中
JimmyJames

@atoth我将编辑您的问题标题以使其更清晰,因为有多个看起来不相关的无关问题(例如Java中的“ RuntimeException”与“ Exception”)。如果您不喜欢新标题,请随时对其进行编辑。
Andres F.

好的,我希望它足够通用,但是如果可以,我可以同意,感谢您对@AndresF的贡献。
atoth,2013年

Answers:


28

异常具有极其有限的语义。必须精确地将它们扔到什么地方,或向上直接调用堆栈中进行处理,并且如果您忘记这样做,则在编译时没有向程序员的指示。

与Elm对比,其中Elm将错误编码为ResultsMaybes,它们都是。这意味着如果不处理该错误,则会出现编译器错误。您可以将它们存储在变量甚至集合中,以将它们的处理推迟到方便的时间。您可以创建一个函数来以特定于应用程序的方式处理错误,而不是到处重复非常相似的try-catch块。您可以将它们链接到一个仅在所有部分都成功的情况下才能成功的计算中,而不必将它们塞满一个try块。您不受内置语法的限制。

这与“吞下异常”完全不同。它使错误条件在类型系统中变得明确,并提供了更灵活的替代语义来处理它们。

考虑以下示例。如果您希望看到它的实际效果,可以将其粘贴到http://elm-lang.org/try中。

import Html exposing (Html, Attribute, beginnerProgram, text, div, input)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import String

main =
  beginnerProgram { model = "", view = view, update = update }

-- UPDATE

type Msg = NewContent String

update (NewContent content) oldContent =
  content

getDefault = Result.withDefault "Please enter an integer" 

double = Result.map (\x -> x*2)

calculate = String.toInt >> double >> Result.map toString >> getDefault

-- VIEW

view content =
  div []
    [ input [ placeholder "Number to double", onInput NewContent, myStyle ] []
    , div [ myStyle ] [ text (calculate content) ]
    ]

myStyle =
  style
    [ ("width", "100%")
    , ("height", "40px")
    , ("padding", "10px 0")
    , ("font-size", "2em")
    , ("text-align", "center")
    ]

请注意,String.toIntcalculate函数有失败的可能。在Java中,这有可能引发运行时异常。在读取用户输入时,它有很大的机会。Elm而是通过返回a来迫使我处理它Result,但是请注意,我不必立即处理它。我可以将输入加倍并将其转换为字符串,然后检查getDefault函数中是否有错误的输入。该位置比发生错误的位置或调用堆栈中向上的位置更适合进行检查。

与Java的检查异常相比,编译器强制我们执行操作的方式也要细得多。您需要使用非常具体的功能,例如Result.withDefault提取所需的值。从技术上讲,您可能会滥用这种机制,但没有多大意义。由于您可以将决定推迟到知道正确的默认/错误消息后再进行,因此没有理由不使用它。


8
That means you get a compiler error if you don't handle the error.-嗯,这就是Java Checked Exceptions背后的原因,但是我们都知道效果如何。
罗伯特·哈维

4
@RobertHarvey在某种程度上,Java的检查异常是可怜人的版本。不幸的是,它们可能会被“吞噬”(例如在OP的示例中)。它们也不是实数类型,使它们成为代码流的多余路径。更好的类型系统的语言允许你的编码错误,一流的价值,这是你在(比如说)做什么用哈斯克尔MaybeEither等榆树看起来像它从语言,如ML,OCaml的或哈斯克尔采取的页面。
安德烈斯F.

3
@JimmyJames不,他们不强迫你。当您要使用该值时,只需要“处理”错误即可。如果我这样做x = some_func(),我不具备做任何事情,除非我要检查的值x,在这种情况下,我可以检查我是否有错误或一个“有效”的价值; 此外,尝试使用一个代替另一个是静态类型错误,所以我不能这样做。如果Elm的类型可以像其他函数语言一样工作,那么实际上我可以做一些事情,例如不知道它们是否是错误之前,编写来自不同函数的值!这是FP语言的典型特征。
安德烈斯F.

6
@atoth但你有显著的收益,有一个很好的理由(如已在多个答案解释你的问题)。我真的鼓励您学习使用类似ML的语法的语言,并且您会发现它摆脱了类似于C的语法残障是多么的解放(顺便说一句,ML是在70年代初期开发的, C的当代作品)。设计这种类型系统的人认为这种语法很普通,这与C不同:)在您使用它的同时,学习Lisp也不会受到损害:)
Andres F.

6
@atoth如果您想从所有这件事中拿走一件事,那就拿这件事:始终确保您不会成为Blub Paradox的牺牲品。不要对新语法感到恼火。也许是因为您不熟悉的强大功能:)
Andres F.

10

为了理解此声明,我们首先必须了解静态类型系统向我们购买了什么。本质上,静态类型系统为我们提供了保证:如果检查程序类型,则不会发生某些类的运行时行为。

听起来不祥。好吧,类型检查器类似于定理检查器。(实际上,根据Curry-Howard-Isomorphism,它们是同一回事。)关于定理的一件事很奇特的事情是,当您证明一个定理时,您便证明了该定理的确切含义,而已。(例如,这就是为什么当有人说“我已经证明该程序正确”时,您应该始终询问“请定义'正确'”。)类型系统也是如此。当我们说“程序是类型安全的”时,我们的意思不是说不可能发生任何错误。我们只能说类型系统向我们承诺要防止的错误不会发生。

因此,程序可以具有无限多种不同的运行时行为。其中,无数个有用,但无数个“不正确”(对于“正确性”的各种定义)。静态类型系统使我们能够证明不会发生那些无限多个错误的运行时行为的某个有限的固定集合。

基本上,不同类型的系统之间的区别在于,可以证明它们不会发生,多少个以及多么复杂的运行时行为。诸如Java之类的弱类型系统只能证明非常基础的东西。例如,Java可以证明类型为返回a的String方法不能返回a List。但是,例如,它不能证明该方法不会返回。也不能证明该方法不会引发异常。它不能证明它不会返回错误 String  -任何String类型都可以满足类型检查器的要求。(当然,即使null将满足它。)甚至有很简单的事情,Java的不能证明,这就是为什么我们有例外,例如ArrayStoreExceptionClassCastException或者大家的喜爱,NullPointerException

像Agda这样的更强大的类型系统也可以证明诸如“将返回两个参数之和”或“返回作为参数传递的列表的排序版本”之类的东西。

现在,Elm的设计者所声明的他们没有运行时异常的意思是,Elm的类型系统可以证明缺少(大部分)运行时行为,而其他语言无法证明这种运行时行为不会发生,因此可能导致到运行时的错误行为(最好的情况是异常,更坏的情况是崩溃,最坏的情况是没有崩溃,没有异常,只是一个默默的错误结果)。

因此,他们并不是说“我们不实施例外”。他们说:“在Elm中,典型的程序员可能会遇到的典型语言中的运行时异常事件将被类型系统捕获”。当然,来自Idris,Agda,Guru,Epigram,Isabelle / HOL,Coq或类似语言的人会发现Elm相形之下比较弱。该语句更针对典型的Java,C ++,C ++,Objective-C,PHP,ECMAScript,Python,Ruby,Perl等语言。


5
潜在编辑者的注意事项:对于双重甚至三重负片的使用,我感到非常抱歉。但是,我故意保留它们:类型系统保证了某些类型的运行时行为的缺失,即它们保证某些事情不会发生。而且我想保持“证明不会发生”的表述不变,这不幸地导致了“无法证明某种方法不会返回”这样的构造。如果您找到改善这些问题的方法,请继续,但是请牢记以上几点。谢谢!
约尔格W¯¯米塔格

2
总体来说,这个答案很好,但是有一个小问题:“类型检查器类似于定理证明器。” 实际上,类型检查器更类似于定理检查器:它们都进行验证,而不是推论
gardenhead

4

出于相同的原因,Elm不能保证没有运行时异常。C可以保证没有运行时异常:该语言不支持异常的概念。

Elm有一种在运行时发信号通知错误情况的方法,但是该系统也不例外,它是“结果”。可能失败的函数将返回“结果”,其中包含常规值或错误。Elms是强类型的,因此这在类型系统中是显式的。如果函数始终返回整数,则其类型为Int。但是,如果返回整数或失败,则返回类型为Result Error Int。(字符串是错误消息。)这将强制您在呼叫站点处显式处理这两种情况。

这是引言中的一个示例(有点简化):

view : String -> String 
view userInputAge =
  case String.toInt userInputAge of
    Err msg ->
        text "Not a valid number!"

    Ok age ->
        text "OK!"

toInt如果输入不可解析,则该函数可能会失败,因此其返回类型为Result String int。要获得实际的整数值,您必须通过模式匹配来“解压缩”,这又迫使您处理这两种情况。

结果和异常从根本上做同样的事情,重要的区别是“默认值”。默认情况下,异常会冒出气泡并终止程序,并且如果要处理它们,则必须明确捕获它们。结果是另一种方式-默认情况下您被迫处理它们,因此如果要它们终止程序,则必须将它们一直传递到顶部。很容易看出这种行为如何导致更健壮的代码。


2
@atoth这是一个例子。想象一下,语言A允许例外。然后,您将获得功能doSomeStuff(x: Int): Int。通常,您希望它返回一个Int,但是它也可以引发异常吗?不查看其源代码,您将不会知道。相反,通过类型对错误进行编码的语言B可能具有相同的声明功能,如下所示:( doSomeStuff(x: Int): ErrorOrResultOfType<Int>在Elm中,该类型实际上称为Result)。与第一种情况不同,现在可以立即清楚该函数是否可能失败,并且您必须显式地对其进行处理。
安德烈斯F.

1
正如@RobertHarvey在对另一个答案的评论中所暗示的那样,这似乎基本上像Java中的检查异常。在早期检查大多数异常的时候,我从使用Java中学到的东西是,您真的不想强迫自己在发生错误时总是编写代码以始终将错误发生。
JimmyJames '16

2
@JimmyJames这与检查的异常不同,因为异常不构成,可以忽略(“吞咽”)并且不是一流的值:)我真的建议学习静态类型的功能语言以真正理解这一点。这是不是有些新奇的东西榆树组成-这是怎么你的语言,如ML或Haskell的程序,它是从Java不同。
安德烈斯F.

2
@AndresF。this is how you program in languages such as ML or Haskell在Haskell,是的。ML,不。罗伯特·哈珀(Robert Harper)是标准ML和编程语言研究人员的主要贡献者,他认为异常很有用。在可以保证不会发生错误的情况下,错误类型会妨碍函数的组合。异常也具有不同的性能。您无需为未抛出的异常付费,但是每次都为检查错误值付费,并且异常是表达某些算法中回溯的自然方法
Doval

2
@JimmyJames希望您现在看到检查的异常和实际的错误类型只是表面上相似。受检查的异常不能很好地结合在一起,使用起来很麻烦,并且不面向表达式(因此,您可以像在Java中那样简单地“吞咽”它们)。未检查的异常不那么麻烦,这就是为什么它们在Java之外都是普遍存在的原因,但它们更有可能使您绊倒,而且您无法通过查看函数声明是否抛出来判断它是否使程序更难了解。
Andres F.

2

首先,请注意,您的“吞咽”异常示例通常是一种可怕的做法,并且与没有运行时异常完全无关。当您考虑它时,确实有一个运行时错误,但是您选择隐藏它而不对其进行任何操作。这通常会导致难以理解的错误。

可以以多种方式解释这个问题,但是由于您在注释中提到了Elm,因此上下文更加清晰。

Elm是一种静态类型的编程语言。这种类型的系统的好处之一是,在实际使用程序之前,编译器会捕获许多错误类别(尽管不是全部)。可以将某些类型的错误编码为类型(例如Elm ResultTask),而不是将其抛出异常。这就是Elm的设计者的意思:许多错误将在编译时而不是在“运行时”被捕获,并且编译器将迫使您处理它们,而不是忽略它们并希望获得最佳结果。很明显为什么这是一个优点:更好的是程序员在用户意识到之前就意识到了问题。

请注意,当您不使用异常时,错误会以其他不太令人惊讶的方式编码。根据Elm的文档

Elm的保证之一是在实践中您不会看到运行时错误。NoRedInk在生产中已经使用Elm大约一年了,但他们仍然没有!像Elm中的所有保证一样,这取决于基本的语言设计选择。在这种情况下,Elm将错误视为数据这一事实为我们提供了帮助。(您是否注意到我们在这里处理了很多数据?)

榆木设计师在主张“无运行时例外”方面有些大胆,尽管他们将其“实际”加以限定。他们可能的意思是“比使用JavaScript进行编码要少的意外错误”。


我是不是看错了,还是他们只是在玩语义游戏?他们取缔了“运行时异常”这个名称,但是只是用一种将错误信息向上传递到堆栈中的机制替换了它。这听起来像只是将异常的实现更改为以不同方式实现错误消息的相似概念或对象。那简直是天翻地覆。就像任何静态类型的语言一样。比较从COM HRESULT到.NET异常的切换。不同的机制,但是无论您如何称呼它,仍然是运行时异常。
Mike

@Mike老实说,我没有详细研究Elm。通过文档来看,他们的类型ResultTask它看起来非常相似,更熟悉Either,并Future从其他语言。与异常不同,可以将这些类型的值组合在一起,并且在某些时候必须明确处理它们:它们代表有效值还是错误?我不介意,但是程序员的惊讶不足可能就是Elm设计师所说的“无运行时异常” :)
Andres F.

@迈克我同意这不是惊天动地。运行时异常的区别在于它们在类型上不是显式的(即,如果不查看源代码,就无法知道一段代码是否会抛出)。类型中的编码错误非常明显,可防止程序员忽略它们,从而导致代码更安全。这是由许多FP语言完成的,实际上并不是什么新鲜事物。
Andres F.

1
根据您的评论,我认为不只是“静态类型检查”。您可以使用Typescript将其添加到JS,这比新的“成败”生态系统的约束要少得多。
atoth,2013年

1
@AndresF .:从技术上讲,Java类型系统的最新功能是参数多态性,该技术可追溯到60年代末。因此,从某种意义上说,当您表示“不是Java”时说“现代”在某种程度上是正确的。
约尔格W¯¯米塔格

0

榆树声称:

实际上没有运行时错误。没有空值。没有未定义不是函数。

但是您会询问运行时异常。有区别。

在榆树中,没有任何东西会返回意外结果。您不能在Elm中编写会导致运行时错误的有效程序。因此,您不需要例外。

因此,问题应该是:

“没有运行时错误”有什么好处?

如果您可以编写永远不会出现运行时错误的代码,则程序将永远不会崩溃。

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.