There is a nice pattern many programs have, which I only noticed after I started to program in Haskell regularly.

If you look at the code path from input to output, you can split up a program into three parts: parsing, transformation, and pretty printing.

Parsing

The first phase, parsing, turns less structured input into well-defined types. The parsing phase on the whole will have a type like:

parse :: ByteString -> Either Error Input

Because we are moving from something unstructured like bytes, there is a possibility the input is invalid. Therefore, parse produces either a well typed Input , or returns an Error .

One of the goals with parse is to catch errors from invalid input as soon as possible.

Transformation

The transformation stage has this general type signature:

transform :: Input -> Output

We design Input with the goal “to make invalid states unrepresentable”. For instance, we could have an options type that looks like:

data Input = Input

{ ludicrousSpeed :: Bool

, suckFlag :: Bool

, randy :: Bool

}

If it is the case that setting ludicrousSpeed = True and suckFlag = True is invalid and would cause a program to error, a better design for the Input type would be:

data Input = LudicrousSpeed { randy :: Bool }

| SuckFlag { randy :: Bool }

This type prevents the invalid state but still let’s us have randy be whatever we want.

If we do our job when designing Input , we can keep transform total (all inputs will have valid outputs without errors). The goal is to force all the possible errors to occur in the parsing stage. It isn’t always practical, but that is the goal.

Pretty Printing

The final stage of the program is to take the highly structured Output and turn it into a less-structured form, like bytes. It has a type signature like:

prettyPrint :: Output -> ByteString

For instance, the ByteString could be a tightly packed binary representation or, I don’t know … JSON (apparently I have to mention JSON in almost every blog post).

Types and Testing

Both parsing and pretty printing deal with less structured data. It is harder for types to help ensure our program is correct in these stages.

Luckily, there is very little complex computation in these stages, and they are fairly straightforward to test.

Most of the heavy lifting happens in the transformation stage, where we have put work into having precise types, which helps limit what we will have to test.

Donesies

This is admittedly an overly simple, idealized model, but I’ve found it helpful when thinking about how to structure my programs and how to test them.