When implementing Conway’s Game of Life for the browser and the terminal with JavaScript, I fell in love with Game of Life. This is my Haskell implementation.

This is the rules of Conway’s Game of Life:
- Any live cell with fewer than two live neighbors dies, as if by under-population.
- Any live cell with two or three live neighbors lives on to the next generation.
- Any live cell with more than three live neighbors dies, as if by overpopulation.
- Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.
I represent Game of Life patterns in a set of text files. A dead cell is represented by a dot, a living cell by an o.
...........
...oo.oo...
...oo.oo...
....o.o....
..o.o.o.o..
..o.o.o.o..
..oo...oo..
...........
...........
Firstly, I format the pattern so that a node contains an integer Point, a tuple with the x/y position, as well as a boolean Status, stating if the node is alive or not. A Node is a tuple with a Point and Status. The list of all existing nodes constitutes the Game. Each game state is a Generation.
type Point = (Int, Int)
type Status = Bool
type Node = (Point, Status)
type Game = [Node]
type Generation = Integer
isCharLiving ∷ Char → Bool
isCharLiving char
| char == 'o' = True
| otherwise = False
makeRow ∷ String → Int → [Node]
makeRow row y =
[((x,y), isCharLiving $ row !! x) | x ← [0..length row - 1]]
prepareData ∷ [String] → Game
prepareData rawData =
concat [ makeRow (rawData !! y) y | y ← [0..length rawData - 1]]
In nextState
I map the list of Nodes, transforming them according to the rules of Game of Life.
nextState ∷ Game → Game
nextState game = map (`makeNode` game) game
When quoting a few Haskell functions, the expressiveness of the type signatures become clear. We pass a Node, and then a Game (the currying is abstracted away in Haskell), and get a new Node.
We don’t care about the Point (it never changes), only the Status. We call nextNodeState
with two (implicitly curried parameters), a function call to aliveNeighbours
(counting the number of living neighbors), and the Status (alive or not).
makeNode ∷ Node → Game → Node
makeNode node game =
(
fst node,
nextNodeState (aliveNeighbours game node directions 0) (snd node)
)
Due to Haskell guards, evaluating predicates, we can formulate the rules very clearly in nextNodeState
,
nextNodeState ∷ Integer → Bool → Bool
nextNodeState aliveNeighbours status
| aliveNeighbours == 3 && not status = True
| aliveNeighbours == 2 && status = True
| aliveNeighbours == 3 && status = True
| otherwise = False
and begin counting neighbors,
aliveNeighbours ∷ Game → Node → [Point] → Integer → Integer
aliveNeighbours game ((x,y), status) dirs count
| null dirs = count
| isAlive game (x + fst (head dirs), y + snd (head dirs))
= aliveNeighbours game ((x,y), status) (tail dirs) (count + 1)
| otherwise = aliveNeighbours game ((x,y), status) (tail dirs) count
directions ∷ [Point]
directions = [(0,-1), (1,-1), (1,0), (1,1), (0,1), (-1,1), (-1,0), (-1,-1)]
isAlive ∷ Game → Point → Bool
isAlive game node
| isNothing(getCell node game) = False
| snd (fromJust(getCell node game)) = True
| otherwise = False
getCell ∷ Point → Game → Maybe Node
getCell pos [] = Nothing
getCell pos (((x,y), status) : rest)
| pos == (x,y) = Just ((x,y), status)
| otherwise = getCell pos rest
This pattern is subsumed the rules and the game continues until the application is ended.
main ∷ IO ()
main = do
rawData ← readFile "./pentadecathlon"
get (prepareData $ lines rawData)
representation ∷ Status → String
representation cell
| cell = "[●]"
| otherwise = "[∙]"
putCell ∷ Cell → IO ()
putCell cell
| fst (fst cell) == 0 = putStr $ "\n" ++ representation (snd cell)
| otherwise = putStr $ representation (snd cell)
clearScreen ∷ IO ()
clearScreen = putStr "\ESC[2J"
get ∷ Game → IO ()
get game = do
sequence_ [putCell cell | cell ← game]
clearScreen
threadDelay 200000
get (nextState game)