Uncrashable languages aren't

A trivial observation, with some examples

elm pony crash

Making buggy situations unrepresentable

Programs have bugs. Both creators and end-users of software dislike bugs. Businesses paying for software development dislike bugs. It's no wonder that, as the role of software in the world expands, we've become very interested in minimizing occurrences of bugs.

One way of reducing bugs is via process: making sure that critical code is tested to the greatest practical extent. Another way is via construction: making sure that buggy code is not representable. This could be achieved by making such code unexpressible in the syntax of a language, or having it fail the compiler's type check.

There are two new programming languages that take a principled stance on the side of non-representability, by preventing code from crashing wantonly: Elm and Pony.

Elm does this by eliminating exceptions from language semantics and forcing the programmer to handle all branches of sum types (i.e your code has to cover all representable states that it might encounter).

Pony does this by allowing anonymous exceptions (the error operator), but forcing the programmer to deal with them at some point. All functions – apart from those which are explicitly marked as capable of throwing errors – MUST be total and always return a value.

A small aside about division by zero

Elm used to crash when you tried to divide by zero. Now (I tried version 0.19), it returns 0 for integer division and Infinity for floating-point division. The division functions are therefore total.

> 5.0 / 0.0
Infinity : Float
> 5 // 0
0 : Int
> remainderBy 5 0
0 : Int
> modBy 5 0
0 : Int

Pony also returns zero when zero is the divisor.

actor Main                                        //  This code prints:
  new create(env: Env) =>                         //  0
   env.out.print((U32(1) / U32(0)).string())

However, Pony also provides partial arithmetic operators (/? for division, +? for addition, below), for when you explicitly need integer over/underflows and division-by-zero to be illegal:

actor Main                                         //  This code prints:
  new create(env: Env) =>                          //  div by zero
    try U32(1) /? U32(0)                           //  overflow
    else env.out.print("div by zero")              //  0
    end                                            //  0
    try U8(255) +? U8(1)
    else env.out.print("overflow")
   env.out.print((U32(1) / U32(0)).string())
   env.out.print((U8(255) + U8(1)).string())

While returning '0' for divison-by-zero is a controversial topic (yet silent integer overflows somehow don't generate the same debate), I think it's reasonable to view this compromise as the necessary cost of eliminating crashes in our code. More interesting is this: we have just made a tradeoff between eliminating crashes and wrong results. Having a total division function eliminates crashes at the cost of allowing wrong results to propagate. Let's dig into this a bit more.

Bugs and the bottom type

Taxonomy is supposed to be the lowest form of science, but let's indulge and distinguish two main types of program misbehavior:

1) A program (or function) produces output which does not match the programmer's intent, design, or specification;

2) A program (or function) fails to produce output (e.g. freezes or crashes)

I hope you'll agree that eliminating 'bugs' caused by the first type of error is not an easy feat, and probably not within the scope of a language runtime or compiler. Carefully designing your data structures to make illegal states unrepresentable may go a long way towards eliminating failures of this kind, as will a good testing regimen. Let's not delve deeper into this category and focus on the second one: functions that crash and never return.

The wikipedia article on the Bottom Type makes for an intersting read. It's nice to conceive of as a hole in the program, where execution stops and meaning collapses. Since the bottom type is a subtype of every type, theoretically any function can return the 'bottom value' — although returning the 'bottom value' acutally means never returning at all.

My claim is that while some languages, like Haskell or Rust, might explicitly embrace the existence of , languages that prevent programmers from 'invoking' the bottom type will always contain inconsistencies (I'm leaving dependently-typed languages out of this). Below are two examples.

Broken promises and infinite loops

Elm's promise is that an application written in Elm will never crash, unless there is a bug in the Elm runtime. There are articles out there that enumerate the various broken edge-cases (regexp, arrays, json decoding), but these cases can arguably be construed as bugs in the runtime or mistakes in library API design. That is, these bugs do not mean that Elm's promise is for naught.

However, if you think about it, an infinite loop is a manifestation of the bottom type just as much as an outright crash, and such a loop is possible in all Turing-complete languages.

Here's a legal Elm app that freezes:

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

main =
  Browser.sandbox { init = 0, update = update, view = view }

type Msg = Increment | Decrement

add : Int -> Int
add n =
  add (n+1)

update msg model =
  case msg of
    Increment ->
      add model

    Decrement ->
      model - 1

view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (String.fromInt model) ]
    , button [ onClick Increment ] [ text "+" ]

What will happen when you click the + button in the browser? This is what:

Screenshot of browser message saying: 'A web page is slowing down your browser.'

The loop is hidden in the add function, which never actually returns an Int. Its true return type, in this program, is precisely . Without explicitly crashing-and-stopping, we've achieved the logical (and type-systematic) equivalent: a freeze.

Galloping forever and ever

The Pony language is susceptible to a similar trick, but we'll have to be a bit more crafty. First of all, Pony does indeed allow the programmer to 'invoke' the Bottom Type, by simply using the keyword error anywhere in a function body. Using this keyword (or calling a partial function) means that we, the programmer, now have a choice to make:

1) Use try/else to handle the possibility of error, and return a sensible default value

2) Mark this function as partial ?, and force callers to deal with the possibility of the Bottom Type rearing its head.

However, we can craft a function that spins endlessly, never exiting, and thus 'returning' the Bottom Type, without the compiler complaining, and without having to annotate it as partial.

Interestingly enough, naïve approaches are optimized away by the compiler, producing surprising result values instead of spinning forever:

actor Main
  new create(env: Env) =>
    let x = spin(false)

  fun spin(n: Bool): Bool =>
    spin(not n)

Before you run this program, think about what, if anything, it should output. Then, run it and see. Seems like magic to me, but I'm guessing this is LLVM detecting the oscillation and producing a 'sensible' value.

We can outsmart the optimizer by farming out the loop to another object:

actor Main
  new create(env: Env) =>
    let t: TarpitTrap = TarpitTrap
    let result = t.spin(true)

class TarpitTrap
  fun spin(n: Bool): Bool =>
    if n then spin(not n)
    else spin(n)

Now, this program properly freezes forever, as intended. Of course this is just a contrived demonstration, but one can imagine an analogous situation happening at run-time, for example when parsing tricky (or malicious) input data.

The snake in the garden

While I enjoy working both in Elm and Pony, I'm not a particular fan of these languages' hard-line stance on making sure programs never crash. As long as infinite loops are expressible in the language, the Bottom Type cannot be excised.

Even without concerns somewhat external to our programming language runtime, such as memory constraints, FFIs, syscalls, or the proverbial admin pulling the plug on our machine (did this really used to happen?), the humble infinite loop ensures that non-termination can never be purged from our (non-dependently-typed) program.

Instead of focusing on preventing crashes in the small, I think we, as programmers, should embrace failure and look at how to deal with error from a higher-level perspective, looking at processes, machines, and entire systems. Erlang and OTP got this right so many years ago. Ensuring the proper operation of a system despite failure is a much more practical goal than vainly trying to expel the infinitely-looping snake from our software garden.