Literate Conway
About a month ago, I sat down to write a Haskell implementation of Conway’s Game of Life.
Taking a literate programming approach, let’s walk through my solution.
First things first, let’s make a Cell type:
|
|
This is nothing more than a simple type alias. The Bool type is a natural way to represent the cell, with live cells being True and dead cells being False. However, this definition allows us to define functions which operate on Cells rather than Bools, which will make the rest of the program much clearer in its intentions.
Next, we will need a function to handle the state transition of a single cell, given its neighbors. In Haskell types, we want:
|
|
That is, next
is a function which takes a Cell and a list of its neighbors
and returns what the Cell should be in the next state of the world.
The rules1 say:
At each step in time, the following transitions occur:
- Any live cell with fewer than two live neighbours dies, as if caused by under-population.
- Any live cell with two or three live neighbours lives on to the next generation.
- Any live cell with more than three live neighbours dies, as if by over-population.
- Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
Since we have completely orthogonal rules for how live cells and dead cells
transition, we can write two definitions for the next
function. First, we
will write the simpler case — the definition for the dead cell.
|
|
This is almost verbatim from the fourth bullet point from Wikipedia, so let’s
move on to the definition of liveCount
, which should return the number of
Cells that are alive in a list. Since Cell is just an alias for Bool, we can
filter out all of the dead cells by using filter
in conjuction with id
. So,
to get the sublist of cells that are alive, we simply:
|
|
Notice that the single argument to liveCells appears only as the last variable in the function definition. We can take advantage of Haskell’s currying, by modifying our function like so:
|
|
This is called Point-Free Style (PFS). It is point-free because we have written the function without referring to its argument, or its “point.”
Now that we have defined liveCells
, all liveCount
needs to do is return the
length of the list returned by liveCells
. In code:
|
|
To simplify this a little, I’m going to remove the liveCells
function
entirely and just use its body. Therefore:
|
|
Now, with one small tweak, we can write liveCount
in PFS as well. We will use
the function composition operator (in Haskell, this is a “.") like so:
|
|
This is what function composition looks like in Haskell. If this is new to you, let me provide an example, in pseudo-Haskell:
|
|
After that lengthy digression, let’s now define the next
function for live
cells. There are three rules, so we will need a few more guard clauses than
before:
|
|
The final rule collapses into the default guard clause, so it does not explicitly appear in the definition. For the sake of clarity, we will assume that the Haskell compiler is smart enough to optimize away the repeated computations of the live neighbor count.
Now that we have finished handling the transition of a single cell, we need to transition the entire world.
Before we can do that, though, we need to be able to represent the world, which
in our case is a two dimensional array. Now, we could represent this using
vanilla nested lists ([[Cell]]
). However, this will likely make the code much
more confusing, and also possibly slower, since we will have to traverse the
same lists over and over. Instead, we will use a package from the Hackage
repositories. You can install it with cabal.
Simply cabal install matrix
. Now we can represent the world as a Matrix,
which we can access by row and column.
We need a function that does a step of the entire world in one fell swoop. We are going to define our function to be:
|
|
The first thing we need to be able to do is get a list of a cell’s neighbors, given its position in the world (or The Matrix, if your name is Neo).
First, let’s define a function that returns a list of coordinates (tuples) of all neighboring positions (including the origin position, for simplicity of expression):
|
|
This will return all the adjacent coordinates, including ones that may not even
be in the world! For example, allNeighborCoordinates 0 0
will include the
coordinate (-1, -1). We need to filter these out, along with the origin
coordinate that will be in the list as well.
Let’s define a function that will determine if a coordinate is valid as a neighbor:
|
|
The order of arguments is actually important here, for a reason we will see in
just a moment. Now we can define the neighborsOf
function:
|
|
Notice how having the coordinate tuple as the last argument to
validNeighborCoordinate
allows us to more easily use it with filter
, rather
than having to define an inline lambda. This has no real advantage as far as I
know, though, other than I like it better and think it looks prettier.
We have done enough set up work that we can define the step
function. I want
to call attention to the mapRow
function defined in the Data.Matrix
module
as so:
|
|
This function transforms a single row in the matrix and returns the entire matrix but with that row changed. So, we can move one row at a time, folding on the intermediate matrices as we go. The only trick is that we want all of the neighbor computations to be based on the original matrix, not any of the intermediate states.
|
|
With that, we have a complete implementation of Conway’s Game of Life.
There is a complete version on my GitHub. There is some minor renaming in an effort to keep some of the lines shorter, but beyond that the code is the same. It also includes some code to handle displaying the world on each iteration, which you will probably be interested in at some point.
Thanks for following along! Drop me a line if you have any questions or feedback.