Nov 17 was the Global Day of Coderetreat. I participated for this kind of event for the first time.
Coderetreat is a day-long, intensive practice event, focusing on the fundamentals of software development and design, away from the pressures of 'getting things done'.
The essence of the format is solving the same problem multiple times in consecutive sessions, each time with a different person.
I can confirm a saying that it's "the opposite of a hackathon". In both you have a chance to reset your mind by adapting a completely different mode of work for the duration of the event. But they’re also on the opposite ends of a spectrum: While hackathons are all about focusing on delivering value quickly (forgoing methodology), during a Coderetreat the focus is on methodology and there’s no value delivered by definition - the problem is chosen to be mundane, and the outcome ends up ritually deleted after each session. The only trace is what you learned.
Coding with a stranger
I found this to be the most entertaining aspect of Coderetreat. Learning to work together with a new person is a Big Thing for me, but also a step out of my comfort zone. This format forces me to take this step multiple times, each time with someone else.
Each session allows to exchange some working habits:
- tool setup (editor, shell, utilities)
- approach towards testing
- rhythm of work (design upfront vs emergent design, …)
Even with short sessions, this ends up effective because you naturally focus on the most important aspect to you.
Interestingly, experience gap in either direction is never an obstacle: either you’ll learn something technical, or you’ll learn to convey ideas better.
A vectorised life
I was worried that implementing the same thing twice will be boring, so I intuitively looked for ways around that. During the first session we implemented Game of Life (this event’s theme) with classic ping-pong approach:
person A writes a test person B writes just enough code to makes it pass person A improves a thing about the code person B writes a test …
A large amount of the time was spent on implementing and testing a
Board class which was really just a wrapper over a 2D array of booleans - it surprised me how many design decisions were there on the way.
In the second session, I wanted to mix things up, so I suggested using an existing board implementation (we picked
numpy) and focusing on operations. While juggling 2D matrices around, we couldn’t help but notice fun things along the way.
We divided the implementation into two steps for easier testing:
- For each cell, calculate the count of neighboring alive cells
- For each cell, given its value and count of neighboring cells, determine its status
Step 1 can be naively implemented using a set of nested
for loops and this is roughly what we initially went for:
def get_neighbour_counts(array): neighbour_indices = [(a, b) for a in (-1, 0, 1) for b in (-1, 0, 1) if a or b] counts = np.zeros(array.shape, dtype=int) range_x = range(array.shape) range_y = range(array.shape) for x in range_x: for y in range_y: neighbours = 0 for dx, dy in neighbour_indices: if x + dx not in range_x or y + dy not in range_y: continue neighbours += array[x + dx, y + dy] counts[x, y] = neighbours return counts
Now for the second step, we observed it’s really a simple logic function (per cell) that we could write and test separately:
def get_next_state(state, neighbours): return (neighbours == 3) or (state and neighbours == 2)
So now we just had to apply that over the whole matrix. But since basic arithmetic in
numpy is broadcast over the whole matrix, we could use the code directly! For example, here’s what happens if you compare an
array to a number in
In : a Out: array([0, 1, 2, 3, 4]) In : a > 1 Out: array([False, False, True, True, True]) In : a == 4 Out: array([False, False, False, False, True])
Given that, we could take the function that worked on scalars and just pass it a matrix instead, relying on duck typing. (We need to change logical operators to equivalent bitwise operators though - this is what
numpy uses for element-wise logic. Also mind the different operator precedence!)
def get_next_state(state, neighbours): return (neighbours == 3) | (state & (neighbours == 2))
Written like this, it can be called either with both scalars and arrays.
Combining the two, the main function looks like:
def life(array): return get_next_state(array, get_neighbour_counts(array))
Having vectorised the second step, we wanted to revisit the first one too. Turns out the operation we wrote earlier was just a crude implementation of matrix convolution which is conveniently available in
scipy. This means we can replace the nested-loop code with a two-liner:
def get_neighbour_counts(array): kernel = np.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]], dtype=int) return convolve2d(array, kernel, mode="same", fillvalue=False)
(At this point I was extremely happy to have written a decent suite of tests. The moment we completely replaced the implementation and saw green tests was the highlight of the evening!)
Bonus find was that we could build different variants of Life by tweaking the variant of convolution used. Here’s a version that assumes all cells outside of the are alive:
def get_neighbour_counts(array): kernel = np.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]], dtype=int) return convolve2d(array, kernel, mode="same", fillvalue=True)
And here’s one that assumes a wrapped (toroidal) surface:
def get_neighbour_counts(array): kernel = np.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]], dtype=int) return convolve2d(array, kernel, mode='same', boundary='wrap')
Here’s the gist with complete code.