I’ll start by defining the data structures and operations that will be used to implement Life. The lazy, compile-time list previously defined will be used heavily. A row of the world is encoded as zipper of cells, with the world grid is simply a zipper of rows. Comonads for both one dimensional and two dimensional zippers make it easy to define and implement the rules of Life.

One Dimensional Zipper

The zipper is a one dimensional structure, a list that also encodes the context of a position in that list. Zippers will be used both as rows of cells, and also to zipper rows to create the vertical dimension of the Life grid.

In our zipper structure, x is the focus, l is a list of elements to the right of the focus, and r is a list of elements to the right of the focus. The first element of both l and r are the direct neighbors of the focus, which requires l to be stored in reverse order.

We change our position in the zippered list by shifting the context in either direction, referred to as left and right movement. The movement operations take the next element from the side of the zippered list we are moving towards. This becomes the new focus, and the old focus is consed onto the other side of the list.

Unbound helper functions that take a zipper are also helpful since they reduce the need using the template and typename keywords. This makes the similarities between C++ templates and more traditional functional languages clearer.

This approach is a bit more verbose, but may make interfaces more clear and explicit. It greatly benefits from the use of helper function that calls fmap by automatically wrapping the callee in a Functor.

The zippers used to implement Life will all be infinite, but to print out the results of the simulation we also need a way to select a finite range of elements from a zipper. to_list takes count elements from the left and right sides of the focus and constructs a list of count * 2 + 1 elements. The left side of the list is stored in reverse order, and must be reversed again during the conversion.

Zipper Comonad

The comonad interface requires the implementation of two operations: extract and either extend or duplicate. extend and duplicate can both be implemented in terms of each other.

For the comonad of a zipper, the extract operation is already defined as get. I’ll use the extend and duplicate comonad definition to complete the interface.

For a zipper, duplicate basically rewrites every value in the zipper to a zipper focused at that value. The result is a zipper of zippers. To actually accomplish this, we take a zipper z and create a new zipper that contains: Zipper<List<left<z>, left<left<z>>, ...>, z, List<right<z>, right<right<z>>, ...>>. The move operation generalizes this pattern for any left and right mapping functions.

Plane Zipper

The zipper is a one dimensional data structure while we need a two-dimensional grid for life. So, much like how you can use an arrays of arrays in C to create a 2D matrix, we’ll use a zipper of zippers to build an infinite grid.

The outermost zipper acts as the vertical axis. Each inner zipper contains a single row of cells of the world. Movement is accomplished by shifting the focus, much like with zipper, but now we can move in four directions: up, down, left, and right.

Movement along the vertical simply moves the outer zipper to focus on a new row. This is accomplished by the up and down operations.

Movement along the horizontal is a bit more complicated. Simply moving the inner zipper left or right, z::get::left or z::get::right, only moves a single row of the grid, leaving all other rows in place. Instead, horizontal movement in the grid must shift all rows, so that the cells remain in the same relative positions, and so that the current row moves to a new focus. Shifting all rows of the grid in one direction or the other is accomplished by a fmap over the outer zipper using the go_left and go_right zipper movement functions.

The actual value at the focus is read by first reading the outer zipper to get the current row zipper and then reading the row zipper. Editing the focus similarly edits the outer zipper to replace the current row, with the current row edited to replace the value at the focus.

PlaneZipper also is a Functor. Mapping a function f over a plane zipper applies f to every value in the grid, building a new grid from the results. For each row in zipper z, we fmap the outer zipper first with a functor do_fmap. do_fmap is applied to every row in the grid and is basically a manually curring of the fmap function, fmapping the row with function f.

Plane Zipper Comonad

The PlaneZipper comonad already implements extend as get. duplicate is implemented by creating a grid of plane zippers focused at each value. vertical create the vertical shift components of this grid, while horizontal creates the rows.

Life

After establishing all of our data structures and operations, implementing life turns out to the easiest part of the whole process.

Conway’s Game of Life is simulated on a 2D, infinite grid of cells. Every cell is either alive or dead. Time is broken into discrete steps or generations. A transition function determines the state of the next generation based entirely on the state of the current generation.

For every cell in the grid, the transition function does the following:

If the cell is alive and it has less than 2 living neighbors, the cell dies.

If the cell is alive and it has 2 or 3 living neighbors, it stays alive.

If the cell is alive and it has more than 3 living neighbors, the cell dies.

If the cell is dead and it has 3 living neighbors, the cell becomes alive.

Cell

Cells are a boolean state of either living or dead. We’ll encode cells using the Cell type, along with aliases for living and dead cell types.

Rules

The next state of a cell is determined solely by the cell’s current state and the state of its direct neighbors. living_neighbors_count takes a plane zipper z and by examines the state of the focus’s eight neighboring cells to determine the total number of living neighboring cells of the focus.

Output

Our compile time Life implementation outputs a type that encodes the state of the game world. This type is completely useless on its own, but we can write simple printing functions that transform the type into a set of operations that print it out at runtime.

Printing of the various types uses the Print struct interface.

template<typename>structPrint;

Print is specialized for each type we are interested in, with specializations implementing a static Do function that writes the value of the input type to stdout.

One-dimensional zippers are printed by converting them to a fixed size list, in this case five elements from each side of the focus for a total of eleven columns, and then printing the contents of the list. Since these zippers form the rows in the Life world, a new line is printed after the contents of the zipper are printed.