I call it my billion-dollar mistake. It was the invention of the null reference in 1965.
—Tony Hoare
I consider exceptions to be no better than “goto’s” ... in that they create an abrupt jump from one point of code to another. In fact they are significantly worse than goto’s:
- They are invisible in the source code.
- They create too many possible exit points for a function.
—Joel Spolsky
numberWord :: Int -> String
numberWord n =
case n of
1 -> "one"
2 -> "two"
Haskell (compiles, throws runtime exception):
[1 of 1] Compiling Main
Ok, modules loaded: Main.
Main> numberWord 2
"two"
Main> numberWord 4
"*** Exception: Main.hs:(3,5)-(5,18): Non-exhaustive patterns in case
Elm (doesn't compile):
-- MISSING PATTERNS --------------------------------------------------- Main.elm
This `case` does not have branches for all possibilities.
You need to account for the following values:
<values besides 1 and 2>
The problem with failing in these ways is that they are inconspicuous (both in the source code and at runtime). Any value may turn out to be null. Any function may throw an exception, or not return a value.
Alternatively, Elm prevents failing in these ways by restricting language features and enforcing constraints.
In Elm
- values cannot be
null
- exceptions cannot be thrown
- functions are pure and must be total
Seek freedom and become captive of your desires. Seek discipline and find your liberty.
—Frank Herbert
We can represent possibly-failed computations with data structures and transform them with functions.
Here are a few parameterized data types from the core library.
type Maybe a
= Just a
| Nothing
type Result x a
= Err x
| Ok a
Maybe
and Result
are transparent types, as their value constructors are exposed, and may be used to construct values or deconstruct values (via pattern matching).
type Decoder a
decodeValue : Decoder a -> Value -> Result String a
type Task err ok
attempt : (Result x a -> msg) -> Task x a -> Cmd msg
Decoder
and Task
are opaque types, as their constructors are not exposed.
Here are a few functions you'll find available for many parameterized data types. They each provide a way to take a value from one type to another.
map : (a -> b) -> T a -> T b
(Maybe, Result, Html, Cmd, List)
map2 : (a -> b -> c) -> T a -> T b -> T c
map3 : (a -> b -> c -> d) -> T a -> T b -> T c -> T d
map4 : (a -> b -> c -> d -> e) -> T a -> T b -> T c -> T d -> T e
mapN
generalizes to:
apply: T (a -> b) -> T a -> T b
(Decoder)
andThen : (a -> T b) -> T a -> T b
Data structures that provide these functions give us a uniform interface for working with failure, collections, and side-effects.
map : (a -> b) -> T a -> T b
apply : T (a -> b) -> T a -> T b
andThen : (a -> T b) -> T a -> T b
Consider the transformation you want to make in terms of its type signature; look for a function having that signature; use that function.