Context/Notions of Computation
Many popular languages lie to you in many ways. An example is what we have seen earlier, where Python functions do not document exceptions in its type signature, and must be separately annotated as a docstrong to denote as such. This is not including the fact that Python type annotations are not enforced at all.
def happy(x: int) -> int:
raise Exception("sad!")
This is not unique to dynamically-typed languages like Python. This is also the case in Java. In Java, checked exceptions must be explicitly reported in a method signature. However, unchecked exceptions, as named, do not need to be reported and are not checked by the Java compiler. That is not to mention other possible "lies", for example, it is possible to return nothing (null
) even if the method's type signature requires it to return "something":
class A {
String something() {
return null;
}
}
We can't lie in Haskell. In the first place, we shouldn't lie in general. What now?
Instead, what we can do is to create the right data structures that represent what is actually returned by each function! In the Python example happy
, what we really wanted to return was either an int
, or an exception. Let us create a data structure that represents this:
data Either a b = Left a -- sad path
| Right b -- happy path
Furthermore, instead of returning null
like in Java, we can create a data structure that represents either something, or nothing:
data Maybe a = Just a -- happy path
| Nothing -- sad path
This allows the happy
and something
functions to be written safely in Haskell as:
happy :: Either String Int
happy = Left "sad!"
something :: Maybe String
something = Nothing
The Maybe
and Either
types act as contexts or notions of computation:
Maybe a
—ana
or nothingEither a b
—eithera
orb
[a]
—a list of possiblea
s (nondeterminism)IO a
—an I/O action resulting ina
These types allow us to accurately describe what our functions are actually doing! Furthermore, these types "wrap" around a type, i.e. For instance, Maybe
, Either a
(for a fixed a
), []
and IO
all have kind * -> *
, and essentially provide some context around a type.
Using these types makes programs clearer! For example, we can use Maybe
to more accurately describe the head
function, which may return nothing if the input list is empty.
head' :: [a] -> Maybe a
head' [] = Nothing
head' (x : _) = x
Alternatively, we can express the fact that dividing by zero should yield an error:
safeDiv :: Int -> Int -> Either String Int
safeDiv x 0 = Left "Cannot divide by zero!"
safediv x y = Right $ x `div` y
These data structures allow our functions to act as branching railways!
head' safeDiv
┏━━━━━ Just a ┏━━━━━ Right Int -- happy path
[a] ━━━━┫ Int, Int ━━━━┫
┗━━━━━ Nothing ┗━━━━━ Left String -- sad path
This is the inspiration behind the name "railway pattern", which is the pattern of using algebraic data types to describe the different possible outputs from a function! This is, in fact, a natural consequence of purely functional programming. Since functions must be pure, it is not possible to define functions that opaquely cause side-effects. Instead, function signatures must be made transparent by using the right data structures.
What, then, is the right data structure to use? It all depends on the notion of computation that you want to express! If you want to produce nothing in some scenarios, use Maybe
. If you want to produce something or something else (like an error), use Either
, so on and so forth!
However, notice that having functions as railways is not very convenient... with the non-railway (and therefore potentially exceptional) head
function, we could compose head
with itself, i.e. head . head :: [[a]] -> a
is perfectly valid. However, we cannot compose head'
with itself, since head'
returns a Maybe a
, which cannot be an argument to head'
.
┏━━━━━ ? ┏━━━━━
━━━━┫ <-----> ━━━━┫
┗━━━━━ ┗━━━━━
How can we make the railway pattern ergonomic enough for us to want to use them?