Updated

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—an a or nothing
  • Either a b—either a or b
  • [a]—a list of possible as (nondeterminism)
  • IO a—an I/O action resulting in a

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?