I’ve used Kotlin for a month to solve Advent of code 2022, and the next few months to solve some leetcode problems. My experience was great so far. Kotlin allows to write concise and safe code. I’ll try to demonstrate that in this post, and to share my BFS template.

What are we doing?

Let’s say we have a problem on 2D grid concerning some cells and their neighbours, like Flood Fill. To apply BFS or DFS in our solution we must take one cell at a time and look at its neighbours. Usually it is easy to iterate over neighbours, but we must check that neighbour exist to not fall through the grid border. In most languages we probably use standard ifs and kinda verbose code like:

int col = cell_x + delta_x;
int row = cell_y + delta_y;

if (0 <= col && col < grid_width &&
    0 <= row && row < grid_height)
{
    // Neighbour at (row, col) exist, we'll process it
}

Python allows us to be more precise and clear:

col = cell_x + delta_x
row = cell_y + delta_y

if 0 <= col < grid_width and 0 <= row < grid_height:
    # Process neighbour at (row, col)

But still, my favourite snippet for the same logic is on Kotlin:

val col = cell_x + delta_x
val row = cell_y + delta_y

if (row in grid.indices && col in grid[0].indices) {
    // Process neighbour at (row, col)
}

We don’t even need to think about exact start and end of a range! indices has type IntRange, and IntRange.contains check is almost like the handwritten check in other languages. The only difference is that end of range is at the last index of container, so we use <= for both ends.

I really liked this part in particular and how the entire solution for Flood fill comes out: data classes, indices, sequences all combine into a thing of beauty. So I decided to share my BFS template for solving such problems with Kotlin.

BFS template

val deltasGrid = listOf(
    0 to -1,
    0 to 1,
    -1 to 0,
    1 to 0)

data class Point(val row: Int, val col: Int)

fun bfs(grid: Array<IntArray>, start: Point) {
    fun getNeighbours(p: Point) = sequence {
        for ((deltaRow, deltaCol) in deltasGrid) {
            val row = p.row + deltaRow
            val col = p.col + deltaCol

            // probably, more checks here before yielding a neighbour
            if (row in grid.indices && col in grid[0].indices) {
                yield(Point(row, col))
            }
        }
    }

    val queue = ArrayDeque<Point>()
    queue.add(start)

    while (queue.isNotEmpty()) {
        val point = queue.removeFirst()

        // process point

        for (neighbour in getNeighbours(point)) {
            queue.add(neighbour)
        }
    }
}