The Structure Of Programs in Haskell
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.