A Google employee, Gregory Collins, responded with a refreshingly clear and simple suggestion:
mainin the IO Monad
undefinedto stub out parts of a program to make it typecheck
Start by modeling the problem in the data domain. Identify the datatypes the program will need. What algebraic properties do they have? For example, if I can identify that a piece of data forms a Monoid, that opens several new options for guiding the design of the implementation — it means that you can aggregate a collection of these objects in parallel, for instance. The same thing goes for identifying Functor, Applicative, Alternative, etc structures.
Once you know roughly what the inputs and outputs will be, and have modeled those types, start thinking about the type of the implementation. Every program starts with
main, and begins life in the IO monad. Optionally, depending on what your program looks like, you may want to lift your computation into some other monad. For example: ReaderT if your program has a global read-only state that should be available everywhere, StateT if you’re using a side-effecting process to mutate or grow a piece of data, or create your own custom monad here.
But stick to plain old IO where possible. The rest of the implementation process is a game of pushing as much of the program as possible into pure functions (they are much easier to test), and breaking problems down into small pieces of structure using the “standard toolbox”. It’s amazing how many computations are expressible using some combination of map/fold/filter/unfold/concat/scanl etc….
I will often stub out parts of the program I know I will need later with
undefinedjust so that I can get a skeleton version of the program to typecheck.