Extreme branchless: Game of Life
In my GDCR Summary, I have mentioned a branchless of the game of life kata.
This logs attempts to present my solution.
First thing first, we have a Cell
which can be either Dead
or Alive
.
With our previously defined framework it gives the following definition:
newtype Cell = Cell (forall a. a -> a -> a)
Then we can add few helpers:
show = runCell "Alive" "Dead"
(==) = (==) `on` runCell True False
runCell a d (Cell f) = f a d
And finally define dead
/alive
:
alive = Cell $ \x _ -> x
dead = Cell $ \_ x -> x
Then we have to think a bit.
The game of life comes with the following rules:
- A living cell with less than 2 living neighbours die
- A living cell with more than 3 living neighbours die
- A living cell with 2 or three living neighbours live
- A dead cell with three living neighbours becomes alive
Let's see how it goes in a table:
Number of living neighbours / Initial state | Dead | Alive |
---|---|---|
< 2 | Dead | Dead |
2 | Dead | Alive |
3 | Alive | Alive |
> 3 | Dead | Dead |
Taking the number of living neighbours as main inputs helps us to pick
a Strategy
which could be represented as:
newtype Strategy = Strategy
{ envolve :: Cell -> Cell
}
We should improve it, especially because at some point, we would need to count the neighbours, as we have seen in the previous log, the best way to deal with numbers is to use Church encoding.
To do so, each Strategy
should be able to know the next Strategy
, the Strategy
with one more living neighbours (it's closed to OOP's Chain of responsibility design pattern):
data Strategy = Strategy
{ name :: String
, envolve :: Cell -> Cell
, nextStrategy :: Strategy
}
Then, we can define the strategies according to our table:
starvation0 =
Strategy {
name = "Starving",
envolve = const dead,
nextStrategy = starvation1
}
starvation1 =
Strategy {
name = "Starving",
envolve = const dead,
nextStrategy = equilibrium
}
equilibrium =
Strategy {
name = "Equilibrium",
envolve = id,
nextStrategy = reproduction
}
reproduction =
Strategy {
name = "Reproduction",
envolve = const alive,
nextStrategy = overpopulation
}
overpopulation =
Strategy {
name = "Overpopulation",
envolve = const dead,
nextStrategy = overpopulation
}
Note:
const x
returns a function which always returnsx
, whatever the input isid
, is the identity function, returning the given parameter
It looks like this:
graph LR; starvation0 --> starvation1; starvation1 --> equilibrium; equilibrium --> reproduction; reproduction --> overpopulation; overpopulation --> overpopulation;
Then we need a way to evolve a whole grid.
There are several ways to represent a grid, I have chosen a Set Position
,
which represents the living cells:
type Position = (Int, Int)
The approach is straightforward:
- For each living cell: list their neighbours
- For each neighbours count their neighbours and select the correct
Strategy
- Give to the
Strategy
the current neighbour state
Which gives:
envolveGrid initialAliveCells = Set.filter isAliveEnvolved $ foldMap listNeighbours initialAliveCells
where isAliveEnvolved :: Position -> Bool
isAliveEnvolved pos = runCell True False $ (selectStrategy pos).envolve $ isAlive pos alive dead
isAlive pos whenAlive whenDead = thenElseIf whenAlive whenDead $ toBool' $ Set.member pos initialAliveCells
selectStrategy = foldl (\acc cell -> isAlive cell (.nextStrategy) id acc) starvation0 . listNeighbours
listNeighbours (x, y) =
Set.fromList [
(x + xDiff, y + yDiff)
| xDiff <- [-1 .. 1]
, yDiff <- [-1 .. 1]
, xDiff /= 0 || yDiff /= 0
]
Notes:
foldMap listNeighbours initialAliveCells
collects all neighboursSet.filter isAliveEnvolved
keep onlyCell
still alive in next generationisAliveEnvolved
apply the selectedStrategy
with current neighbour's state and convert it toBool
forfilter
isAlive
check if a cell is alive in the current grid, there's much boilerplate because I use the usualSet
, I have to cheat a bit hereselectStrategy
list the potential neighbours and on them, starting from a 0-neighbour strategy, for eachPosition
, if theCell
is dead, we keep the currentStrategy
, otherwise, pick thenextStrategy
listNeighbours
generates+1
/-1
Position
graph TD; Position -- is alive --> nextStrategy Position -- is dead --> id
And that's it.
Note: here's my Bool'
wrapper:
newtype Bool' = Bool' (forall a. a -> a -> a)
show = thenElseIf "True" "False"
(==) = (==) `on` thenElseIf True False
thenElseIf t f (Bool' g) = g t f
toBool' x = if x then true else false
true = Bool' $ \x _ -> x
false = Bool' $ \_ x -> x