I’m starting to pick up Haskell again, and this time it’s finally clicking for me. Believe it or not, Learn You A Haskell explains monads in a way that actually makes sense! While I’m learning Haskell, I’m going to be making some dumb little projects. What better way to make sure I understand how they work than by explaining them on here? And who knows, maybe you’ll catch the Haskell bug, too.

The first project is a simple implementation of Conway’s game of life. The full source is at GitHub. There isn’t much to life, really. All we have to do is read or generate a starting grid, apply the rules to make grid N+1 from grid N, then draw a grid on the screen.

Read or generate a starting grid

I represent a grid of cells as a vector1 of Bool along with a width. We need the width to convert the flat list of cells into a 2D grid. Generating a World from a list of bools is easy:

newWorld’ 5 (V.fromList . map Cell $ cells)

To read a user-defined starting grid, I opted for simple. If there is a command-line argument, we pretend it’s a filename open it. The file can have anything, but only 1′s and 0′s count: 1′s become living cells, 0′s become dead cells. The length of the first line is the width of the grid. There are two sample files in the repo to play with.

The contents of the file are sent to deserializeWorld:

deserializeWorld :: String -> IO World
deserializeWorld packed =
  let toCell ch = Cell $ ch == ‘1
    valid ch = ch == ‘1‘ || ch == ‘0
    cells = V.fromList . map toCell . filter valid $ packed
    width = length . takeWhile (/= ‘\n’) $ packed
  in return $! newWorld’ width cells

Invalid characters are filtered, then the rest are converted to Cells and turned into a vector. There are different ways to get the width. Something like this might work better:

width = length . head . lines $ packed

Also, deserializeWorld doesn’t need to be a monad, it just makes main a little cleaner.

Apply the rules

The rules are applied at each step by mapping the function ageCell over each cell in the grid.

stepWorld :: World -> World
stepWorld world =
  world {population = mapWorld (ageCell world) world}

ageCell :: World -> Position -> Cell -> Cell
ageCell world pos (Cell cell_live) =
  let liveNeighbors = countLiveCells $ neighborCells world pos
    lonely = liveNeighbors < 2
    crowded = liveNeighbors > 3
    spawn = liveNeighbors == 3 && not cell_live
  in Cell $ spawn || (not lonely && not crowded && cell_live)

The function countLiveCells is a straightforward fold, while neighborCells generates a list of adjacent cell positions, converts them to indices, then maps the vector index operator over those. The only tricky bit is filtering out positions off the edge of the grid, which is done in this excerpt from neighborCells

inBounds xx yy = xx >= 0 && xx < width &&
  yy >= 0 && (idxFromPos width (xx, yy)) < len
adjacentIndices =
  [idxFromPos width (px + x, py + y) |
    x <- deltas, y <- deltas,
    not (x == 0 && y == 0), inBounds (px + x) (py + y)]

That function is damn ugly, and there must be a better way to do it. If you have any ideas, leave a comment or fork my repo.

Draw a grid

I used the package gloss for drawing. It make graphics in Haskell super easy, and it automatically handles viewport controls: you can drag, zoom, and rotate the viewport with the mouse. Look how easy it is to draw the whole world:

renderWorld :: World -> Picture
renderWorld = Pictures . V.toList . mapWorld renderCell

renderCell :: Position -> Cell -> Picture
renderCell (xx, yy) (Cell live)
  | live = Translate (fromIntegral xx) (fromIntegral yy) $ rectangleSolid 1 1
  | otherwise = Blank

What do we do with our Picture now? It goes back to one of gloss’s window functions. We’re using a function called simulateInWindow that will create a window, step your simulation at a given frame rate, then render and display each frame. See, easy!

So this is pretty much the bare minimum for life in Haskell. There are a few simple improvements to make and some more complicated ones. It would be simple to make the framerate and view scale command-line arguments. Adding a parser for Alan Hensel’s pattern format would be great, but complicated,2 and I’m sure there are performance improvements galore. You can get the full source here.

If you have any suggestions, I’d love to hear from you. Or you can fork my repository and let the world see your genius.

  1. Data.Vector 

  2. The parsec package is a good place to start if you’re writing a parser.