Question

Some languages claim to have "no runtime errors" as a clear advantage over other languages that has them.

I am confused on the matter.

Runtime error is just a tool, as far as I know, and when used well:

  • you can communicate "dirty" states (throwing at unexpected data)
  • by adding stack you can point to the chain of error
  • you can distinguish between clutter (e.g. returning an empty value on invalid input) and unsafe usage that needs attention of a developer (e.g. throwing exception on invalid input)
  • you can add detail to your error with the exception message providing further helpful details helping debugging efforts (theoretically)

On the other hand I find really hard to debug a software that "swallows" error. E.g.

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

So the question is: what is the strong, theoretical advantage of having "no runtime errors"?


Example

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

No runtime errors in practice. No null. No undefined is not a function.

Était-ce utile?

La solution

Exceptions have extremely limiting semantics. They must be handled exactly where they are thrown, or in the direct call stack upwards, and there is no indication to the programmer at compile time if you forget to do so.

Contrast this with Elm where errors are encoded as Results or Maybes, which are both values. That means you get a compiler error if you don't handle the error. You can store them in a variable or even a collection to defer their handling to a convenient time. You can create a function to handle the errors in an application-specific manner instead of repeating very similar try-catch blocks all over the place. You can chain them into a computation that succeeds only if all its parts succeeds, and they don't have to be crammed into one try block. You are not limited by the built-in syntax.

This is nothing like "swallowing exceptions." It's making error conditions explicit in the type system and providing much more flexible alternative semantics to handle them.

Consider the following example. You can paste this into http://elm-lang.org/try if you would like to see it in action.

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")
    ]

Note the String.toInt in the calculate function has the possibility of failing. In Java, this has the potential to throw a runtime exception. As it reads user input, it has a fairly good chance of it. Elm instead forces me to deal with it by returning a Result, but notice I don't have to deal with it right away. I can double the input and convert it to a string, then check for bad input in the getDefault function. This place is much better suited for the check than either the point where the error occurred or upwards in the call stack.

The way the compiler forces our hand is also much finer-grained than Java's checked exceptions. You need to use a very specific function like Result.withDefault to extract the value you want. While technically you could abuse that sort of mechanism, there isn't much point. Since you can defer the decision until you know a good default/error message to put, there's no reason not to use it.

Autres conseils

In order to understand this statement, we first have to understand what a static type system buys us. In essence, what a static type system gives us, is a guarantee: iff the program type checks, a certain class of runtime behaviors cannot occur.

That sounds ominous. Well, a type checker is similar to a theorem checker. (Actually, per the Curry-Howard-Isomorphism, they are the same thing.) One thing that is very peculiar about theorems is that when you prove a theorem, you prove exactly what the theorem says, no more. (That's for example, why, when someone says "I have proven this program correct", you should always ask "please define 'correct'".) The same is true for type systems. When we say "a program is type-safe", what we mean is not that no possible error can occur. We can only say that the errors the type system promises us to prevent can't occur.

So, programs can have infinitely many different runtime behaviors. Of those, infinitely many ones are useful, but also infinitely many ones are "incorrect" (for various definitions of "correctness"). A static type system allows us to prove that a certain finite, fixed set of those infinitely many incorrect runtime behaviors cannot occur.

The difference between different type systems is basically in which, how many, and how complex runtime behaviors they can prove to not occur. Weak type systems such as Java's can only prove very basic things. For example, Java can prove that a method that is typed as returning a String cannot return a List. But, for example, it can not prove that the method won't not return. It can also not prove that the method won't throw an exception. And it cannot prove that it won't return the wrong String – any String will satisfy the type checker. (And, of course, even null will satisfy it as well.) There are even very simple things that Java cannot prove, which is why we have exceptions such as ArrayStoreException, ClassCastException, or everybody's favorite, the NullPointerException.

More powerful type systems like Agda's can also prove things like "will return the sum of the two arguments" or "returns the sorted version of the list passed as an argument".

Now, what the designers of Elm mean by the statement that they have no runtime exceptions is that Elm's type system can prove the absence of (a significant portion of) runtime behaviors that in other languages can not be proven to not occur and thus might lead to erroneous behavior at runtime (which in the best case means an exception, in a worse case means a crash, and in the worst case of all means no crash, no exception, and just a silently wrong result).

So, they are not saying "we don't implement exceptions". They are saying "things that would be runtime exceptions in typical languages that typical programmers coming to Elm would have experience with, are caught by the type system". Of course, someone coming from Idris, Agda, Guru, Epigram, Isabelle/HOL, Coq, or similar languages will see Elm as pretty weak in comparison. The statement is more aimed at typical Java, C♯, C++, Objective-C, PHP, ECMAScript, Python, Ruby, Perl, … programmers.

Elm can guarantee no runtime exception for the same reason C can guarantee no runtime exception: The language does not support the concept of exceptions.

Elm has a way of signalling error conditions at runtime, but this system it is not exceptions, it is "Results". A function which may fail returns a "Result" which contains either a regular value or an error. Elms is strongly typed, so this is explicit in the type system. If a function always return an integer, it has the type Int. But if it either returns an integer or fails, the return type is Result Error Int. (The string is the error message.) This forces you to explicitly handle both cases at the call site.

Here is an example from the introduction (a bit simplified):

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

    Ok age ->
        text "OK!"

The function toInt may fail if the input is not parseable, so its return type is Result String int. To get the actual integer value, you have to "unpack" via pattern matching, which in turn forces you to handle both cases.

Results and exceptions fundamentally does the same thing, the important difference is the "defaults". Exceptions will bubble up and terminate the program by default, and you have to explicitly catch them if you want to handle them. Result is the other way - you are forced to handle them by default, so you have to expliitly pass them all the way to the top if you want them to terminate the program. It is easy to see how this behavor may lead to more robust code.

First, please note that your example of "swallowing" exceptions is in general a terrible practice and completely unrelated to having no run time exceptions; when you think about it, you did have a run time error, but you chose to hide it and do nothing about it. This will often result in bugs which are hard to understand.

This question could be interpreted in any number of ways, but since you mentioned Elm in the comments, the context is clearer.

Elm is, among other things, a statically typed programming language. One of the benefits of this kind of type systems is that many classes of errors (though not all) are caught by the compiler, before the program is actually used. Some kinds of errors can be encoded in types (such as Elm's Result and Task), instead of being thrown as exceptions. This is what the designers of Elm mean: many errors will be caught at compile time instead of at "run time", and the compiler will force you to deal with them instead of ignoring them and hoping for the best. It's clear why this is an advantage: better that the programmer becomes aware of a problem before the user does.

Note that when you don't use exceptions, errors are encoded in other, less surprising ways. From Elm's documentation:

One of the guarantees of Elm is that you will not see runtime errors in practice. NoRedInk has been using Elm in production for about a year now, and they still have not had one! Like all guarantees in Elm, this comes down to fundamental language design choices. In this case, we are helped by the fact that Elm treats errors as data. (Have you noticed we make things data a lot here?)

Elm designers are a bit bold in claiming "no run time exceptions", though they qualify it with "in practice". What they likely mean is "less unexpected errors than if you were coding in javascript".

Elm claims:

No runtime errors in practice. No null. No undefined is not a function.

But you ask about runtime exceptions. There's a difference.

In Elm, nothing returns an unexpected result. You can NOT write a valid program in Elm that produces runtime errors. Thus, you don't need exceptions.

So, the question should be:

What is the benefit of having "no runtime errors"?

If you can write code that never has runtime errors, your programs will never crash.

Licencié sous: CC-BY-SA avec attribution
scroll top