Conway’s Game Of Life is a cellular automaton: just a very simple model of computation that pretends to emulate “life”. It consists of a grid of cells each one of which can be alive or dead. Taken from Wikipedia these are the rules:
At each step in time, the following transitions occur:
- Any live cell with fewer than two live neighbours dies, as if caused by underpopulation.
- 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 overpopulation.
- Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
You can see it live here (the column at the right side):
That was an art installation called With a Rhythmic Instinction to be Able to Travel Beyond Existing Forces of Life by Phillippe Parreno. You can even see it slows down from time to time when too much “collisions” happen.
So I built the Game Of Life using Elm (code is here). Elm is a functional language that targets front-end applications. It’s similar to Haskell but it doesn’t have type classes. It has a very dogmatic perspective on how to build applications (the so called Elm architecture) which relies in function composition to produce components composition. You can play around with my implementation here:
It gets old very quickly.
So, while building it I learned a bunch of things:
The core logic
As it turns out the core logic (the step rules) are very easy to implement.
Starting with some type definitions:
type alias Cell =
: Bool
{ active : Int
, x : Int
, y
}
type alias Grid =
Array (Array Cell)
Then the core logic is here:
: Grid -> Cell -> Cell
updateCell =
updateCell grid cell let
=
neighbours
getNeighbours cell grid
=
liveNeighbours <| List.filter .active neighbours
List.length
=
newState if cell.active then
== 2 || liveNeighbours == 3
liveNeighbours else
== 3
liveNeighbours in
| active = newState }
{ cell
: Grid -> Grid
step =
step grid Array.map (Array.map (updateCell grid)) grid
You can see the other functions here. Those are also kind of simple.
With this it’s very easy to create the update
and view
functions of the Elm architecture.
Rendering
I decided to use plain HTML elements because tracking which cell was clicked in a canvas is harder. In Elm every graphical element is rendered according to a function you describe, there are no templates or anything like that. The good thing about this is that you can continue to leverage the language when describing the views.
Starting with your domain types like Cell
you build an Html from it with a function, say cellView: Cell -> Html Msg
. Then for a composed type like Grid
you create a function gridView : Grid -> Html Msg
by using the cellView
function we built before. Thus, graphical component composition is achieved by composing functions which is something very simple! I think that idea is powerful, useful and simple.
Random grid generation
One interesting challenge was how to generate a random configuration of the grid with some of the cells alive and others dead. Elm’s Random library has a very nice approach, which is usual in functional languages. For a boolean value they could have provided a function that, when called, would return a random boolean. The problem is that this function wouldn’t be purely functional because different invocations would result in different values being returned. A better way is to return something that can generate a random value. And then, when we want to generate a random value we use this generator to create a Cmd Msg
value which we are going to give to Elm’s runtime. It will execute the random generation (that’s executing a side effect, possibly changing the state of a random seed) and will deliver us (through the update
function) the result.
Talking in code this is what we do:
-- length of the square grid
= 25
n
type Msg
= ...
| ...
| GenerateRandomGrid
| SetRandomGrid Grid
: Msg -> Model -> ( Model, Cmd Msg )
update =
update msg model case msg of
...
GenerateRandomGrid ->
! [ generateRandomGrid ]
model
SetRandomGrid randGrid ->
| grid = randGrid } ! []
{ model
: Cmd Msg
generateRandomGrid =
generateRandomGrid SetRandomGrid (randomGrid n)
Random.generate
: Int -> Generator Grid
randomGrid = ??? randomGrid n
Random.generate
is the function that “performs” the side effect. It creates a Cmd Msg
that Elm’s runtime receives, produces a random value according to the generator and hands it back trough the update
function.
The strategy of delaying some computation and returning a representation of it is very common in functional programming. This allows us to have very declarative combinator functions.
The question then is: how to build a Generator Grid
? To do this we can start with something that generates a random boolean and from that we can build a Generator Cell
:
: Int -> Int -> Generator Cell
randomCell =
randomCell x y -> Cell active x y) Random.bool Random.map (\active
Random.map
is one of those useful combinator functions I was talking about. It receives two things: a function and a generator and returns a new generator that applies the input function to the generated values.
Now let’s say we want to generate a row of n
cells with some index x
. We could try something like this:
: Int -> Int -> Generator (Array Cell)
randomRow =
randomRow n x
randomCell x|> Array.repeat n
But we are returning a Array (Int -> Generator Cell)
! We can apply the index for each element like this:
: Int -> Int -> Generator (Array Cell)
randomRow =
randomRow n x
randomCell x|> Array.repeat n
|> Array.indexedMap (\y getCellGen -> getCellGen y)
A smart-ass way to do the same:
: Int -> Int -> Generator (Array Cell)
randomRow =
randomRow n x
randomCell x|> Array.repeat n
|> Array.indexedMap (|>)
But we are still not returning a Generator (Array Cell)
, we are returning a Array (Generator Cell)
! Does this sound like a familiar problem? It turns out that this is a very common combinator in functional programming. In other languages this type flipping function (we want to exchange the types Generator
and Array
) is called sequence
. Unfortunately the standard library doesn’t have it, but Random.Extra
does. It’s called together
there and it works on List
, not Array
.
Aside (Click!)
In Haskell the function that turns a [m a]
into a m [a]
(similar to what we want to do here) is called sequence
. I think it’s more common to use mapM
, though. In Haskell there are type classes so this function can be generalized for every type in the Applicative type class (Haskell restricts to Monad but this is a historical error). In Elm this function must be implemented explicitly for each type that has the same interface as an Applicative.
Thus, we can get what we want like this:
: Int -> Int -> Generator (Array Cell)
randomRow =
randomRow n x
randomCell x|> List.repeat n
|> List.indexedMap (|>)
|> Random.Extra.together
|> Random.map Array.fromList
Now we want to use this function to generate a random grid of cells, given a number n
(the number of rows and columns). Curiously the code is very similar:
: Int -> Generator Grid
randomGrid =
randomGrid n
(randomRow n)|> List.repeat n
|> List.indexedMap (|>)
|> Random.Extra.together
|> Random.map Array.fromList
Coloring patterns
During a run some interesting patterns can arise. There is even a wiki section classifying a lot of them. So I decided to give some specific color when some of these patterns arises.
To do this I took a very simplistic approach:
- Identify contiguous regions of live cells.
- For each of these regions try to match it against a set of templates of patterns.
- Those regions that matched some pattern will be shown with a different color.
To do the first step I had to find the connected components of live cells in the grid. I implemented Breadth First Search (BFS) from a specific cell like this and to identify the contiguous regions of live cells I run it repeatedly like this. Maybe for BFS I’m lacking a proper queue but other than that it didn’t turn out that difficult to implement that algorithm in a functional language.
Next I try to match these regions with respect to a set of pattern’s templates. First I defined a template as a non empty list of templates and a template is just a non empty list of coordinates. These coordinates should be ordered by increasing x
and then by increasing y
(this makes it easier a later comparison). These are the types:
type alias Template =
Cons ( Int, Int )
type alias Pattern =
Cons Template
Cons
is just the type of a non empty list, taken from the library with the same name.
To match a region against a pattern, we normalize the region by translating it to the origin as close as we can and we look for a template that matches. If some of the pattern’s templates is equal to the translated region then it’s a match:
: Pattern -> Region -> Bool
matchesPattern pattern region =
matchesPattern let
=
minx
region|> Cons.map .x
|> Cons.minimum
=
miny
region|> Cons.map .y
|> Cons.minimum
=
normalized
region|> Cons.map (\{ x, y } -> ( x - minx, y - miny ))
in
-> p == normalized) pattern Cons.any (\p
This may not be the most efficient way to do this but it’s the first thing that came to my mind.
And finally to find out which color, if any, a region must have I wrote something like this:
: List Pattern
patterns = ...
patterns
: List Color
colorPalette = ...
colorPalette
: List ( Pattern, Color )
patternsAndColors =
patternsAndColors
List.Extra.zip patterns colorPalette
: Region -> Maybe Color
getRegionColor =
getRegionColor region pattern, _ ) -> matchesPattern pattern region) patternsAndColors
find (\( |> Maybe.map (\( _, color ) -> color)
: List Region -> List ( Region, Color )
getRegionsColors =
getRegionsColors
List.filterMap->
(\region
getRegionColor region|> Maybe.map (\color -> region => color)
)
Conclusion
This was a fun exercise! I enjoyed very much programming in Elm! This exercise didn’t have too many side effects, though. Just the random grid generation so I don’t know if that model scales to “real” applications. Also this project didn’t involve that much component composition (not only composing the views but also the update
functions and the Model
types). Nevertheless, Elm is a very cool language and the Elm architecture is fun to follow. I’m not sure if it allows all the kind of patterns that can arise when building more complex applications. But unlike, say Angular, you can understand what’s going at each part of your application. No magic binding or anything like that. I don’t think I will use Elm at my job but I hope to learn more about it in the future.