Mastering Kotlin indexed access operator
Tags: readability, kotlin
Published: 2020-05-24 13:44:12


![Should be replaced with indexing - Replace 'set' call with indexing operator](refactoring-with-indexed-access-operator.png)

Indexed access operator in Kotlin provides an easy-to-read syntax for random-access like data structures like Array, List and Map, ... But Kotlin is powerful enough that we can implement our own indexed access operator for our custom data structure. In this post, we will walk through a chessboard example to understand better this operator and also [operator overloading](https://kotlinlang.org/docs/reference/operator-overloading.html) in general.

## You might have seen it, many times

If you have ever called the **set**/**get** method on a List/Array/Map on Kotlin, 

```kotlin
nutritionFacts.set("Saturated fat", 10.grams)

// and later on
nutritionFacts.get("Saturated fat")
```

you might have encountered this suggestion from IntelliJ/Android Studio: "Should be replaced with indexing ...". And if you accept the suggestion, your code might turn out to be like this:

```kotlin
nutritionFacts["Saturated fat"] = 10.grams

// and retrieve
val quantity = nutritionFacts["Saturated fat"]
```

This situation can be seen often when you copy a code in Java then convert it to Kotlin in IntelliJ/Android Studio. The pair of brackets **\[ ]** which encapsulate an index value (a string "Saturated fat" in the previous example) is the indexed access operator. It serves generally two purposes: **ge**t (retrieve) and **set** (mutate) value of an index. The set part is of course only applicable for mutable collections (Array, MutableList, HashMap, ...).

This operator is very obvious if you are using a collection type and key value of some primitive types (Int, String, ...). But what if we want the to use this operator on a data structure of our own? 

In the next sections, we will work on a data structure which represents a [chess](https://en.wikipedia.org/wiki/Chess) board and we will implement this operator on this data structure to simplify access

## We start with a chess board model

```kotlin
enum class Kind {
    KING, QUEEN, ROOK, BISHOP, KNIGHTS, PAWN
}

enum class Color {
    BLACK, WHITE
}

data class Piece(
    val color: Color, 
    val kind: Kind
)

class ChessBoard {
    private val squares: Array<Array<Piece?>> = Array(SIZE) {
        Array(SIZE) { null }
    }

    companion object {
        const val SIZE: Int = 8
    }
}
```

We then need a way to set/assign a piece to a square in a chess board. It is necessary if we want to implement higher level operation (move, checkmate, ...). A simplest way could be:

```kotlin
class ChessBoard {
    fun set(row: Int, column: Int, piece: Piece?) {
        squares[row][column] = piece
    }
}

val blackRook = Piece(BLACK, ROOK)
board.set(0, 7, blackRook) // assign blackRook to square (0, 7)
// later
board.set(0, 7, null) // remove the rook
```

Even if the **set** method is easy to understand for a developer, it is not obvious for a chess player. We should always aim to use [the same language as domain experts](https://martinfowler.com/bliki/UbiquitousLanguage.html). Otherwise, we also have leaked the implementation detail (array index based for row and column) to our API (**set** method). And the **set** method signature does not restrict the value for row and column.

## Refactoring with indexed access operator

The standard method for recording moves in chess is [algebraic notation](https://en.wikipedia.org/wiki/Algebraic_notation_(chess)), meaning using pairs like b4, e6, ... to identify squares in the board. To be coherent with the game we will you this notation in our code.

```kotlin
// Ranks (rows) denoted 1 to 8 from bottom to top according to White's perspective
//
// Raw value of rank is used as array index in chessboard. It is reversed compared to rank
// because rows in array is 0 to 7 from top to bottom
enum class Rank(internal val rawValue: Int) { 
    ONE(7), TWO(6), THREE(5), FOUR(4), FIVE(3), SIX(2), SEVEN(1), EIGHT(0)
}

// Columns
enum class File(internal val rawValue: Int) {
    A(0), B(1), C(2), D(3), E(4), F(5), G(6), H(7)
}
```

Then we can update our **set** method: 

```kotlin
class ChessBoard {
    fun set(file: File, rank: Rank, piece: Piece?) {
        squares[rank.rawValue][file.rawValue] = piece
    }
}

val blackRook = Piece(BLACK, ROOK)
board.set(H, ONE, blackRook) // assign blackRook to square h1
```

Now we have a better method signature, if we can think about the indexing syntax:

```kotlin
board[H, ONE] = blackRook
```

by just adding **operator** keyword to our **set** method and voilà. Yes, that's easy as pie 😉.

```kotlin
class ChessBoard {
    operator fun set(file: File, rank: Rank, piece: Piece?) {
        squares[rank.rawValue][file.rawValue] = piece
    }
}
```

Behind the scene, the operator keyword and the method name **set** is telling Kotlin compiler that we are defining the behavior of indexed access operator for our class Chessboard. The name **set** is mandatory for this operator.

And we can now define the accessor method (get) with the same idea:

```kotlin
class ChessBoard {
    operator fun get(file: File, rank: Rank): Piece? {
        return squares[rank.rawValue][file.rawValue]
    }
}

val piece = board[H, ONE]
```

## Alternative implementation

Depending on your taste, you might prefer 1D index to 2D one, like

```kotlin
val piece = board[sqr(H, ONE)]
```

We just need a little adaptation for previous code:

```kotlin
data class Square(
    val file: File, 
    val rank: Rank
)

fun sqr(file: File, rank: Rank): Square {
    return Square(file, rank)
}

class Chessboard {
    operator fun set(square: Square, piece: Piece?) {
        squares[square.rank.rawValue][square.file.rawValue] = piece
    }

    operator fun get(square: Square): Piece? {
        return squares[square.rank.rawValue][square.file.rawValue]
    }
}
```

## Conclusion

Indexed access operator is a great improvement for container-like data structures with random access characteristic to increase the readability of your code. In this post, we've gone through implementations of 2D and 1D index for a custom data structure. You have now all it takes to implement your own.

Indexed access operator is one specific case of [operator overloading](https://kotlinlang.org/docs/reference/operator-overloading.html). If done incorrectly or used abusively, instead of making our code easier to read, it can lead to very unnecessary cryptic code. As with great power comes great responsibility, we should use them with caution.