How to structure your Haskell program

How to structure medium or big Haskell programs? Through my journey using Haskell, I devised some basic structures. See also my post on “donts” with Haskell. You can find the source code of this blog post on github.

Basic program structure

This is my basic program structure:

- README.md - myprog.cabal - stack.yaml - AUTHORS - LICENCE - TODO - src - Myprog - Main.hs - Game.hs - Settings.hs - Types.hs - Utils.hs - SubPart.hs - SubPart - Thing.hs - Types.hs - Utils.hs

Use a Types module

I found that defining a “Types” module, containing all the types of your program, is helpful. See for example this module. On the contrary, most libraries that you find on Hackage defines the types in the same files where they are used. However, I found that putting all the types in the same “Types” module allows to give a big picture of the program. Furthermore it helps avoids cycle dependencies.

Example of Types.hs file:

{-# LANGUAGE TemplateHaskell #-} module Types where import Control.Lens -- | Informations on a particular game data Game = Game { _gameName :: String , _settings :: Settings } deriving ( Show , Eq ) -- | settings data Settings = Settings { _login :: String , _avatar :: FilePath } deriving ( Show , Eq ) 'GameInfo makeLenses ' 'Settings makeLenses '

This files defines the Game and the Settings data structures. You can immediatly see the dependencies between the two. The functions working on these can be defined in different files, for instance Game.hs and Settings.hs .

State Monad with Lens

Structure your program around a State Monad, and access it via Lenses. I found it the most efficient way of structuring big programs. Each functions can deal only with a sub-part of the data structure.

Using the example above, here is the Game.hs file:

module Game where import Settings startGame :: StateT Game IO () () = do startGame $ putStrLn "Starting Game" liftIO <- zoom settings checkSettings reszoom settings checkSettings if res res then putStrLn "Settings OK" else putStrLn "Wrong settings"

And the Settings.hs file:

module Settings where checkSettings :: StateT Settings IO Bool = do checkSettings log <- use login use login return ( length log /= 0 )

I use a StateT monad transformer to express the type of my function: StateT Settings IO Bool . The Settings is the data structure containing the state. Using IO as a monad allows you to perform additional IO operations with liftIO . If you don’t need IO in you operations, use simply State Settings Bool . The Bool is the return type, in case your function needed to return something. To access/update the state, use Lenses. Lenses allows you to access you data structure in a very handy way. For small programs it’s overkill, but as soon as your data structure will grow bigger with many levels of depth, it’s absolutly necessary.

Utils module

Another useful module is “Utils”. I put here all functions that does not fit anywhere basically, but are of general utility, for example concatMapM. It’s cleaner to avoid this module it of course, if you can, but not always possible…

Split your program as you go

Start small and, as soon as your program gets too big, split it. 200 lines of Haskell is already quite a lot for the same file. In the same order of ideas, when the program gets too big for a single package, consider creating some libraries. A package with 20 .hs files is already quite a lot, in my opinion.

Use re-exports

If some part of your program becomes big, it makes sense to create a sub-folder, or even a library. In both cases I create a module with the same name as the folder/library, containing re-exports. You can re-export only the functions that are useful to your users, leaving all internal functions hidden. That allows to:

allows users of your library/program to import it with one import , instead of importing each modules.

, instead of importing each modules. hide the internals.

This module contains only exported function names and the imported modules:

module Engine (gravity, rendering) where import Engine.Mechanics import Engine.Rendering

Use type synonyms

Type synonyms are very useful to avoid confusion and potential bugs. I usually define type synonyms when my functions use common types such as String or Int as parameters.

type GameName = String type PlayerName = String addPlayerToGame :: PlayerName -> GameName -> StateT Game IO () addPlayerToGame pn gn = ...

Using the type synonims can avoid confusing a player name and a game name. Since the visual information is carried by the types, the variable names can stay short. If you don’t use the type synonyms, you need to keep long variable names such as playerName and gameName .

Comments