Operator overloading is one of Kotlin’s most beloved features. It lets you give familiar syntax to custom types, and you’re probably already using it without even noticing it. Take the Map interface, for example: the indexed access operator ([]) is used to read and write entries:

val m = mutableMapOf<Int, String>()
m[1] = "one"     // m.put(1, "one")
val value = m[1] // m.get(1)

The same syntax works for arrays, lists, and any type that declares an operator fun get or operator fun set. Did you know that you can overload the indexed access operator with multiple indices?

Example: A two-dimensional grid that stores its data in a one-dimensional Array

A common scenario where multi-index access is handy is a two-dimensional grid. Below is a Grid class that keeps its state in a single Array and exposes a natural grid[x, y] syntax for both reading and writing.

class Grid(val width: Int, val height: Int) {
    /** Create a one-dimensional array, initially filled with all zeroes */
    private val data = Array(width * height) { 0 }

    /** Convert (x, y) to the underlying one-dimensional index */
    private fun index(x: Int, y: Int): Int = y * width + x

    /** Read the element at (x, y), or return `null` for non-existing coordinates */
    operator fun get(x: Int, y: Int): Int? = data.getOrNull(index(x, y))

    /** Write a new value at (x, y) */
    operator fun set(x: Int, y: Int, value: Int) {
        require(x in 0..<grid.width && y in 0..<grid.height)
        data[index(x, y)] = value
    }
}

Using the Grid

val grid = Grid(width = 3, height = 2) // 3 columns × 2 rows

grid[1, 0] = 42     // Set cell (1, 0) to value 42
println(grid[1, 0]) // → 42

// Print the entire grid
for (y in 0..<grid.height) {
    for (x in 0..<grid.width) {
        print("${grid[x, y]} ")
    }
    println()
}

With the operator fun get and operator fun set signatures, the compiler simply translates grid[x, y] int a call to grid.get(x, y). Similarly, grid[x, y] = v translates to grid.set(x, y, v). Multiple indices are just multiple parameters.

In this example, I am using Int parameters only, but this can be anything. For example, it is perfectly valid to write operator fun get(x: String, y: Int): Double { …​ }, and call the function like this: grid["hello", 5]. The corresponding set function would be: operator fun set(x: String, y: Int, value: Double) { …​ }

Why overload with multiple indices?

Using a multi-index signature makes the grid’s API feel more natural: grid[x, y] reads like "the cell at (x, y)", while keeping the underlying one-dimensional array hidden from callers, so they don’t need to worry about row-major ordering. This encapsulation also gives you the flexibility to swap out the storage strategy later without touching any client code.

Summary

Kotlin’s indexed access operator can be overloaded for any number of indices (e.g. a 3D grid with an x, y, and z coordinate). A 2D grid backed by a 1D array is a perfect example: just declare operator fun get(x, y) and operator fun set(x, y, value) and you get a clean, idiomatic API.

Shoutout to Alexander Chatzizacharias for reminding me of this awesome Kotlin feature.

shadow-left