Conway's Game of Life using Kotlin and Arrow
by Jorge Castillo
- •
- December 12, 2019
- •
- kotlin• functional programming• arrow
- |
- 13 minutes to read.

This article is part of our series on Functional Programming solutions for the Global Day of Coderetreat challenge. Feel free to take a look to our other posts in the series, where we solve the problem using Swift, Haskell, and Scala.
The Global Day of Coderetreat asked participants to solve the Conway’s Game of Life challenge using various languages. In this post, we’ll showcase a basic approach for it using Kotlin and Functional Programming provided by the Arrow library.
When I looked at this problem, the first thought that came to my mind was an evolving program state. I decided to use this opportunity to showcase the State
Monad a bit, and, more specifically, the StateT
Monad Transformer. But, first things first, let’s take a look at our problem domain.
Modelling our domain
If you read the problem description, you’ll notice we are going to need some cells. Given a Cell
is restricted to having only one of two possible types, we can use a Kotlin sealed class
to model it.
sealed class Cell {
abstract val id: String
data class Alive(override val id: String) : Cell() {
constructor() : this(generateId())
}
data class Dead(override val id: String) : Cell() {
constructor() : this(generateId())
}
}
We give each cell a different identity so we can differentiate them and compare using equals if required in tests. Since data classes
in Kotlin are considered value objects, they’re defined by their properties. So the way to make them unique is to give them a proper identity. By doing this, we will avoid falling into scenarios where many different Cells could be considered the same just because all of them are Alive
or Dead
, for example.
For simplicity, we will generate our ids using UUID.randomUUID().toString()
.
When it comes to modeling our Universe
, since it’s described as a grid of two dimensions where each position is considered a Cell
, we will use a simple typealias
.
typealias Universe = List<List<Cell>>
Given we’ve got direct interoperability between List
and Arrow’s ListK
(its Higher Kind counterpart), this is probably the most obvious way to model it.
Finally, since we will also need to reason about positions more than once, it seems convenient to model them as a class (they could also be simple tuples).
data class Position(val x: Int, val y: Int)
Writing tests first
As explained in the beginning, by reading the problem description, we will find a clearly defined program state that gets generated multiple times sequentially, and, for each generation, it must satisfy a constrained set of rules:
- Any live cell with two or three neighbors survives.
- Any dead cell with three live neighbors becomes a live cell.
- Any live cell with fewer than two live neighbors dies, as if by underpopulation.
- Any live cell with more than three live neighbors dies, as if by overpopulation.
We’ve got four very well-defined properties that could become very straightforward tests we could write to ensure our program does what’s expected.
This looks like a perfect situation to benefit from writing tests first, then solving our problem. That’ll probably shortcut some problems beforehand. To write those, we’ll be using KotlinTest and property based testing. You can take a look to those here.
As a summary, we generate universes with dimension 3x3, given that’s the minimum Universe
we’d need to validate the required properties, since those are based on the state of the current Cell
and all its neighbors (surrounding ones). The Universe
generator looks like this:
class UniverseGen(private val center: Cell, private val aliveNeighbors: Int) : Gen<Universe> {
override fun constants(): Iterable<Universe> = emptyList()
override fun random(): Sequence<Universe> = generateSequence {
val alive = (1..aliveNeighbors).map { Cell.Alive() }.toList()
val dead = (1..(8 - aliveNeighbors)).map { Cell.Dead() }.toList()
val all = alive + dead
all.shuffled().toGrid(center)
}
private fun List<Cell>.toGrid(center: Cell): List<List<Cell>> =
listOf(
listOf(get(0), get(1), get(2)),
listOf(get(3), center, get(4)),
listOf(get(5), get(6), get(7))
)
}
fun universeGenWith2or3AliveNeighbours(cell: Cell) =
Gen.choose(2, 3).flatMap { UniverseGen(cell, it) }
fun universeGenFewerThan2AliveNeighbours(cell: Cell) =
Gen.choose(0, 1).flatMap { UniverseGen(cell, it) }
fun universeGenMoreThan3AliveNeighbours(cell: Cell) =
Gen.choose(4, 8).flatMap { UniverseGen(cell, it) }
We pass a Cell
to use as the center (Position(1, 1)
) and that we’ll use as the reference for our tests to assert over, then generate up to eight neighbors around it with a random state each (Alive
or Dead
).
We also provide some handy methods to shortcut some specific scenarios we’ll be interested in for testing our given properties.
For more details, again, please don’t hesitate to take a look at our tests.
Our solution
Thinking back to the problem definition, we identify a State
that’s generated based on the previous State
of the program on each tick. That’s essentially the definition of the State
Monad, since it wraps a computation with the type (S) -> Tuple2<S, A>
, where S
is the input and output state, and A
is the potential result of the computation, if needed.
In this case, we’ll just be interested in the actual evolving state, so we can use Unit
for A
.
We can achieve sequential generations by using flatMap
, which is what it’s defined for. We have State in Arrow, so we could definitely go with it and encode our solution maybe like this:
fun gameOfLife(currentGeneration: Int = 0, maxGenerations: Int): State<Universe, Unit> =
State { universe: Universe ->
universe.tick()
}.flatMap(idMonad) {
if (currentGeneration < maxGenerations) {
gameOfLife(currentGeneration + 1, maxGenerations)
} else {
State.just(idMonad, it)
}
}
But we’ll also want to log the current state for each tick, so we’ll have some side effects, and State
is not defined to handle those. If we just logged within the State
invoke call along with the tick()
, the program could potentially blow up without any control.
Other types like IO
are defined to control effects. There’s actually a pretty interesting solution that just uses IO
by @pedrovgs. Take a look at it since it’s another valid approach.
In our case, we’ll use this chance to show a different alternative; not better, not worse, just different for academic purposes. We’ll be using StateT
, the state transformer.
StateT
is defined as StateT<F, S, A>
, so it can work over any F
. We’ll fix F
to be IO
here, since that will give us the chance to protect us against the side effects using the powers of IO
, but also to transform it, bringing the capabilities of State
to it so we can use them to model our evolving program state.
This is how our Game of Life solution looks:
fun gameOfLife(maxGenerations: GenerationCount = Infinite, currentGeneration: Int = 0): StateT<ForIO, Universe, Unit> =
StateT(IO.monad()) { universe: Universe ->
IO {
println(universe)
universe.tick()
}
}.flatMap(IO.monad()) {
when (maxGenerations) {
is Infinite -> gameOfLife(maxGenerations, currentGeneration + 1)
is Finite -> if (currentGeneration < maxGenerations.count - 1) {
gameOfLife(maxGenerations, currentGeneration + 1)
} else {
StateT.just(IO.monad(), it)
}
}
}
We log the universe, then tick (so we also log the initial state). Then we can flatMap
the program over itself so we get a potentially infinite recursion that’s lazily evaluated (one tick at a time) thanks to the inherent capabilities of State
(it wraps a computation that requires some state to run, hence it waits for it).
We also used the chance to improve the maxGenerations
a bit by using a sealed class that can be Finite
or Infinite
. The Finite
one will be very handy in tests since we’ll just need a few generations to ensure the program works and complies to the required laws.
Of course the tick()
function encodes all the program requirements, like:
private fun Universe.tick(): Tuple2<Universe, Unit> {
val newGeneration = this.map { column ->
column.map { cell ->
val aliveNeighbors = cell.aliveNeighbours(this).size
when {
aliveNeighbors < 2 || aliveNeighbors > 3 -> Cell.Dead()
aliveNeighbors == 3 && cell.isDead() -> Cell.Alive()
else -> Cell.Alive()
}
}.k()
}.k()
return newGeneration toT Unit
}
The rules are very self descriptive.
To get the neighbors of a cell, we use the powers of ListK
cartesian product to get the potential deltas, then filterMap
the ones that don’t have valid positions on the grid (e.g.: some positions around the edges).
fun Cell.neighbours(universe: Universe): List<Cell> {
val allDeltaCombinations = listOf(-1, 0, 1) * listOf(-1, 0, 1) // ListK cartesian product to get all combinations
val deltas = allDeltaCombinations - Tuple2(0, 0)
return deltas
.filterMap { universe.cellPosition(this).shiftBy(universe, it) }
.map { universe[it.x][it.y] }
}
We can use filterMap
because we return Option
from cellPosition
and shiftBy
, so non valid positions are returned as None
, hence filtered out. You can dig into the details in the source code.
Running the program
Given that State
and StateT
require an initial state to run, we can pass it using the convenient run
function provided for it. Since our StateT
returns an IO
, we’ll also need to unsafely run it in this case.
fun main() {
gameOfLife(Finite(3)).run(IO.monad(), initialSeed()).fix().unsafeRunSync()
}
Here we are constraining the max number of generations to 3
.
Conclusions
You can take a look at the entire implementation at my Conway’s Game of Life repository.
We hope you enjoyed this exercise, and we look forward to seeing your takes on this challenge using Kotlin and Arrow. So please, share them with us!
Please check out the following Arrow resources. Comments and questions are welcome and encouraged!
- arrow-kt.io
- @arrow_kt on Twitter
- Arrow Gitter Channel
- My Twitter profile
- My blog where I post a lot about Functional Programming.
The active development of Arrow is proudly sponsored by Xebia Functional, formerly 47 Degrees, a Functional Programming consultancy with a focus on the Scala, Kotlin, Haskell, and Swift Programming languages.