Hi, I am Saša Jurić, a software developer with 10+ years of professional experience in programming of web and desktop applications using Elixir, Erlang, Ruby, JavaScript, C# and C++. I'm also the author of the upcoming Elixir in Action book. In this blog you can read about Erlang and other programming related topics. You can subscribe to the feed, follow me on Twitter or fork me on GitHub.

About a month ago, on Elixir Quiz site there was a Conway's Game of Life challenge. While I didn't find the time to participate in the challenge, I played with the problem recently, and found it very interesting.

My solution is simplified in that I deal only with square grids. It's not very hard to extend it to work for any rectangle, but I wanted to keep things simple.

Functional abstraction

The whole game revolves around the grid of cells which are in some state (dead or alive), and there are clear rules that determine the next state of each cell based on the current state of its neighbours. Thus, I've implemented the Conway.Grid module that models the grid. Let's see how the module will be used.

As can be deducted from the second example, a cell state can be either zero (not alive) or one (alive).

Once the grid is instantiated, we can move it a step forward with Conway.Grid.next/1:

1

grid=Conway.Grid.next(grid)

Finally, we can query grid's size, and the value of each cell:

1
2
3
4

Conway.Grid.size(grid)# Returns 0 or 1 for the cell at the given locationConway.Grid.cell_status(grid,x,y)

This is all we need to manipulate the grid and somehow display it.

This is a simple decoupling technique. The game logic is contained in the single module, but the "driving" part of the game, i.e. the code that repeatedly moves the game forward, is left out.

This allows us to use the core game module in different contexts. In my example, I'm using Conway.Gridfrom a simplistic terminal client, but it's easy to use the module from a GenServer for example to push updates to various connected clients, or from unit tests to verify that state transition works properly.

Another nice benefit of this approach is that we can use :erlang.term_to_binary/1 to serialize the structure and persist the grid state, and then later deserialize it and resume playing the grid.

This is what I like to call a functional abstraction. Notice in previous examples how we use Conway.Grid without knowing its internal representation. The module abstracts away its internal details. In particular, as clients, we don't care what data type is used for the module. All we know that creator and updater functions return a "grid", and all functions from Conway.Grid know how to work with that grid.

The module thus abstracts some concept, and does so relying on a pure functional (immutable) data structure. Hence, a functional abstraction.

Note: Frequently, the term type is used for this. I'm not particular fan of this terminology. To me, the only true Elixir types are the ones supported by BEAM. All others, such as HashDict, HashSet, Range, Erlang's :gb_trees, and even structs, are somehow composed from those basic types.

Choosing the data representation

Update: As Greg and leikind pointed out in comments, the approach I'm taking here is neither efficient nor flexible, because I'm keeping and processing all cells, instead of dealing only with live ones. You can find the alternative version, where only live cells are kept in a HashSethere. The nice thing is that the change was simple, due to abstraction of the Conway.Grid. The module interface remained the same.

In any case, let's start implementing Conway.Grid. The most important decision is how to represent the grid data. Given the game rules, we have following needs:

random access to cells (their states)

incremental building of the grid

We need the first property to access neighbour cells when determining the next state of each cell. The second property is needed since in each step we fully rebuild the grid based on the current state of each cell.

Performance wise, this is probably not the optimal implementation, but I think it's a reasonable first attempt that still keeps the code simple and clear.

So to recap, out grid will be implemented as the tuple of tuples:

1
2
3
4
5

{{1,1,0},{0,1,0},{1,1,0}}

This is all the data we need, since we can efficiently derive the grid size from the data via Kernel.tuple_size/1. It's still worth making our Conway.Grid a struct, so we can gain pattern matching, possible polymorphism, and easier extensibility.

Hence, the skeleton of the module will look like:

1
2
3
4
5

defmoduleConway.Griddodefstructdata:nil...end

Now we can start implementing the module.

Constructing the grid

Recall from usage examples that our "constructor" function is overloaded. It either takes a grid dimension and creates the randomly populated grid, or it takes a list of lists with prepopulated data.

Here, we take the desired size, and produce a square list of lists, calling the producer_fun lambda for each element. Then, we just pass it to list_to_data/1 to convert to a tuple of tuples. This genericity of new_data/2 will allow us to reuse the code when moving the grid to the next state.

Here I've resorted to a case branch, because I think it's the most readable approach in this case. I've experimented with moving this branching to a separate multiclause, but then it was less clear what is being pattern-matched.

Counting alive neighbours

Now we move to the most complex part of the code. Calculating the number of alive neighbours. For this, we have to get the state of each surrounding cell, and count the number of those which are alive.

In this example, I've decided to use the for comprehension, because it has nice support for multiple generators and rich filters.

However, for emits results to a collectable, and we need a single integer (the count of alive neighbours). Therefore, I've implemented a simple sum collectable. It allows us to collect an enumerable of numbers into an integer containing their sum.

The idea is then to use for to filter all alive neighbours, emit value 1 for each such neighbour, and collect those 1s into a Sum instance:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

defpalive_neighbours(grid,cell_x,cell_y)do# 1. Iterate all x,y in -1..+1 areaforx<-(cell_x-1)..(cell_x+1),y<-(cell_y-1)..(cell_y+1),(# take only valid coordinatesxin0..(size(grid)-1)andyin0..(size(grid)-1)and# don't include the current cell(x!=cell_xory!=cell_y)and# take only alive cellscell_status(grid,x,y)==1),# collect to Suminto:%Sum{}do1# add 1 for every alive neighbourend|>Sum.value# get the sum valueend

I did initial implementation of this with nested Enum.reduce/3 and I wasn't as pleased. This solution actually takes more LOC, but I find it easier to understand. There are many other ways of implementing this counting, but to me this approach seems pretty readable. YMMV of course.

Update: Tallak Tveide rightfully asked why not just pipe the result of for into Enum.sum/1 (note also that Enum.count/1 also works). This will work, and quite possibly perform just fine. However, when I was first writing this particular function, I asked myself why would I want to create an intermediate enumerable just to count its size. This is why I made the Sum collectable. It's probably over-engineering / micro-optimizing for this case, but I found it an interesting exercise. As an added benefit, I have a generic Sum collectable which I can use in any of my code whenever I need to count the number of filtered items.

In any case, we're done. The simple implementation of Conway's Game of Life is finished. We have a nice functional abstraction and a basic terminal client. Give it a try on your machine. Just paste the complete code into the iex shell, or run it with elixir conway.ex.

8
comments:

My very first elixir program was Conway's Game of Life. I've never liked grid based solutions. Instead each generation is a list of tuples of live cells. I think this simplifies a lot of code. You can see it here if interested https://github.com/gvaughn/elixir_kata/tree/master/conway

I agree with Greg. A grid is the first idea that comes to mind, but it is not efficient memory-wise (why store uninhabited cells?), and you are limited to the size of your matrix. Storing coordinates of live cells in a set or a hash is more interesting. It also allows you to create multidimensional worlds. Here's my implementation in (rather ugly) Erlang that I wrote back in 2011 : https://gist.github.com/leikind/1338236 , and in (far more readable) Ruby: https://gist.github.com/leikind/1130653

They also have programmable world rules, that is, you can change when a cell dies or comes to life

Thank you for the comment! I'm amazed at how much there is to learn from such a seemingly simple problem. I'll keep the post as it is, but I'll add a comment about raw matrix being inefficient.

I definitely agree that keeping just the list of alive cells is much better. To be honest, I didn't even think about alternative algorithms, and wanted instead to focus on abstracting the grid. The nice thing about it is that I could vary the implementation without changing the interface.

A simple take where I keep just alive cells can be found on https://gist.github.com/sasa1977/7d101a5698edfd6b0dc9I still keep the notion of boundaries. This can be changed, but would also require a change to the interface, since the display code is currently coupled to this notion.

That will work, but it requires another pass through the generated intermediate list. The %Sum{} doesn't require building an intermediate list and passing it one more time. Granted, it's probably not significant for such simple grids. However, if the list we're filtering and counting were larger, %Sum{} approach would definitely perform better.