June 29, 2018

PFP: The Billion Dollar Mistake

Welcome to Part One of Practical Functional Programming (PFP). To learn more about the origin of this series, read the Introduction: Practical Functional Programming: Prelude.


To get warmed up, let’s talk about one of the classic problems of programming.

Problem

Your app has unexpected runtime errors due to null (or undefined.)

Background: History

Tony Hoare famously called null references his Billion Dollar Mistake. In addition to null, in JavaScript we have undefined as in the dreaded

'undefined' is not a function

PureScript doesn’t have runtime errors caused by null references. Let’s see why that is.

Example

Code: JavaScript (broken)

When we try to find an element in an array that doesn’t exist we get undefined back. Accessing numWorldCupTitles on undefined (or null) throws a runtime error (line 15):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const teams = [
  { numWorldCupTitles: 5, country: "Brazil" },
  { numWorldCupTitles: 4, country: "Germany" },
  { numWorldCupTitles: 4, country: "Italy" },
  { numWorldCupTitles: 2, country: "Uruguay" },
  { numWorldCupTitles: 2, country: "Argentina" },
  { numWorldCupTitles: 1, country: "England" },
  { numWorldCupTitles: 1, country: "Spain" },
  { numWorldCupTitles: 1, country: "France" }
];

const switzerland = teams.find(
  team => team.country === "Switzerland"
);
console.log("Switzerland: " + switzerland.numWorldCupTitles);

// TypeError: Cannot read property 'numWorldCupTitles'
// of undefined

Unfortunately, we won’t know that until we run that particular line of code, either manually or by writing a test first.

Cause

Accessing properties on null or undefined throws a runtime error.

Solution

What if we ditched null as a first-class language feature? Or never even introduce it in the first place? That’s exactly what PureScript does.

Code: PureScript (broken)

This PureScript program is equivalent to the JavaScript above:

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
teams = [
  { numWorldCupTitles: 5, country: "Brazil" },
  { numWorldCupTitles: 4, country: "Germany" },
  { numWorldCupTitles: 4, country: "Italy" },
  { numWorldCupTitles: 2, country: "Uruguay" },
  { numWorldCupTitles: 2, country: "Argentina" },
  { numWorldCupTitles: 1, country: "England" },
  { numWorldCupTitles: 1, country: "Spain" },
  { numWorldCupTitles: 1, country: "France" }
]

main = do
  let switzerland = Array.find (\team ->
                      team.country == "Switzerland") teams
  Console.log ("Switzerland: " <>
    show switzerland.numWorldCupTitles)

Before we can run a PureScript program, it first gets checked by the compiler. The compiler will refuse to compile the program and return the following error:

Could not match type

  Record

with type

  Maybe

Above, Record—think of it as Object in JavaScript—refers to one of our array entries. We tried to access numWorldCupTitles on Record but Array.find returned Maybe Record which doesn’t have such a field. The reason we get Maybe Record instead of Record is because under the hood, PureScript’s Array.find has the following type (slightly simplified):

Array.find :: forall a. (a -> Boolean)
                     -> Array a
                     -> Maybe a
                     -- ^^^^^^^

We can ignore the beginning and just focus on the bit after the last arrow. That marks the function’s return type: Maybe a.

The equivalent in TypeScript is:

type find<A> = (predicate: (element: A) => boolean)
            => (array: Array<A>)
            => Maybe<A>;
            // ^^^^^^^^

What is Maybe?

Maybe is a data type to describe whether a value is present or not. Here’s how it’s defined in PureScript:

-- | The `Maybe` type is used to represent optional values and
-- | can be seen as something like a type-safe `null`, where
-- | `Nothing` is `null` and `Just x` is the non-null value `x`.
data Maybe a = Nothing | Just a

The a parameter in Maybe a can refer to any type, e.g. a built-in String, Boolean, Number type, or your own custom WorldCupTeam type. If the syntax is unfamiliar to you, a very literal interpretation of the above in TypeScript is:

type Maybe<A> = { type: "Nothing" } | { type: "Just"; value: A };

So what’s special about Maybe? Well, nothing (no pun intended) really, except that it forces you to be explicit about which values are always required…

name      :: String
birthYear :: Number

…versus ones that are optional:

streetName   :: Maybe String
annualSalary :: Maybe Number

Based on this, the compiler will let you know if you didn’t handle both cases, Just and Nothing. In the example above, we could fix it as follows:

Code: PureScript (fixed)

By adding a case expression, we can independently handle Just and Nothing:

20
21
22
23
24
25
26
27
28
29
main = do
  let maybeSwitzerland = Array.find (\team ->
                          team.country == "Switzerland") teams
  case maybeSwitzerland of
    Just switzerland ->
      Console.log (
        "Switzerland: " <> show switzerland.numWorldCupTitles)
    _ -> Console.log "Switzerland has never won a World Cup."

-- Switzerland has never won a World Cup.

If the value of maybeSwitzerland matches the pattern Just switzerland, we extract the switzerland value (a Record) and log its numWorldCupTitles value. Otherwise, we log an alternate message.

Conclusion

Unhandled nulls and undefineds can cause unexpected runtime errors.

By adopting language with a sufficiently expressive type system such as PureScript, you can explicitly model the presence and absence of values and enforce handling of all cases while avoiding the problems of null.


Enjoyed this post? Follow me on Twitter @gasi to learn when the next one is out.


Notes

  • You may think: Doesn’t TypeScript alleviate this problem with strict null checks (--strictNullChecks compiler option)? You’re right.

    However, please keep in mind that this required the TypeScript team to update the compiler and if you were a TypeScript 1.0 (released in April 2014) user, you would have had to wait almost two and a half years until TypeScript 2.0 (released in September 2016) to leverage this. Due to its design, PureScript supported this basically from day one.

    Future posts will have more examples—async / await among others—of how a simple core language with custom operators and a powerful type system allows PureScript developers to solve many issues themselves that require JavaScript or TypeScript developers to wait for their respective compiler—Babel or tsc—to support.

  • Have you noticed anything strange about the PureScript code listing above, besides the maybe unfamiliar syntax? It has no explicit type definitions. Odd for a post about the power of types, no? Indeed.

    One of the many cool things about PureScript (and Haskell) is that it can fully infer all the types in your program. But since types are useful to see when sharing code with your coworkers (or yourself in three months from now), not writing type definitions on top-level definitions results in a compiler warning. Therefore, here’s the whole listing with types added and zero compile warnings:

Code: PureScript (full listing)

Note the explicitly added type signatures for top-level definitions on lines 11, 13, and 26:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
module PFP1WorldCup3 where

import Prelude

import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE)
import Control.Monad.Eff.Console as Console
import Data.Array as Array
import Data.Maybe (Maybe(..))

type Team = { numWorldCupTitles :: Int, country :: String }
-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
teams :: Array Team
-- ^^^^^^^^^^^^^^^^
teams = [
  { numWorldCupTitles: 5, country: "Brazil" },
  { numWorldCupTitles: 4, country: "Germany" },
  { numWorldCupTitles: 4, country: "Italy" },
  { numWorldCupTitles: 2, country: "Uruguay" },
  { numWorldCupTitles: 2, country: "Argentina" },
  { numWorldCupTitles: 1, country: "England" },
  { numWorldCupTitles: 1, country: "Spain" },
  { numWorldCupTitles: 1, country: "France" }
]

main :: forall eff. Eff (console :: CONSOLE | eff) Unit
-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
main = do
  let maybeSwitzerland = Array.find (\team ->
                           team.country == "Switzerland") teams
  case maybeSwitzerland of
    Just switzerland ->
      Console.log (
        "Switzerland: " <> show switzerland.numWorldCupTitles)
    _ -> Console.log "Switzerland has never won a World Cup."

-- Switzerland has never won a World Cup.

Thanks to Aseem, Boris, Gerd, Matt, Shaniece, and Stephanie for reading drafts of this.