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 tonull
, in JavaScript we haveundefined
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 getundefined
back. AccessingnumWorldCupTitles
onundefined
(ornull
) 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 handleJust
andNothing
:
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 patternJust switzerland
, we extract theswitzerland
value (aRecord
) and log itsnumWorldCupTitles
value. Otherwise, we log an alternate message.
Conclusion
Unhandled null
s and undefined
s 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 ortsc
—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.