Operator overloading in Kotlin

Operator overloading in Kotlin

Operator overloading is a feature you might rarely need in an average program, but it’s a delight to have when you actually require it. While Kotlin isn’t as powerful in this regard as languages like C++ or Haskell, it still enables some neat and practical implementations.

One notable design flaw in Java’s evolution of data structures is the inconsistent treatment of arrays. Despite having native syntax support, arrays are seldom used in modern Java because Collections and Streams are far more powerful and versatile, yet they do not profit from native syntax support and are purely based on objects and methods.

Kotlin addresses this issue by introducing square bracket syntax for Collections. This allows you to write list[index] instead of invoking list.get(index) or list.set(index, value) explicitly.

The Challenge

Recently, I’ve been working extensively on my password manager. It is command-based, and many commands can operate on up to 10 slots, each represented by the digits 0-9.

Here’s an example: Passwords can be organized into namespaces to group them logically. There’s a default namespace (slot 0) and up to nine additional named spaces. Most commands operate within the current namespace. For instance, if you have both personal and work-related GitHub accounts, you can give them the same identifier by placing them in separate namespaces.

To manage instances of a generic type across 10 fixed slots, I needed a class where the indices are immutable. If the instance in slot 4 is deleted, slot 5 remains untouched rather than shifting to fill the gap. Furthermore, it should be possible to refer to a slot by either its integer index or an existing enum constant. Finally, the class should behave like any collection, supporting operations like forEach, map, and filter.

The solution

Let’s dive into the implementation.

This class header ensures the class can manage arbitrary types, be iterated over, and remain extensible:

open class Slots<T> : Iterable<MutableOption<T>>        

I chose a fixed-size array as the internal data structure. This design makes the structure itself immutable while allowing the managed instances to be updated safely, minimizing the risk of dereferencing null:

private val items: Array<MutableOption<T>> = Array(10) { 
  mutableOptionOf()
}        

Next, we need to overload the get and set operators and implement the Iterable interface:

operator fun get(index: Int) = requireValidIndex(index).run {
  items[index]
}
operator fun get(slot: Slot) = get(slot.index())

operator fun set(index: Int, value: T)
  = requireValidIndex(index)
    .run { items[index].set(value) }
operator fun set(slot: Slot, value: T)
  = set(slot.index(), value)
operator fun set(index: Int, valueOption: Option<T>)
  = valueOption.ifPresent { set(index, it) }
operator fun set(slot: Slot, valueOption: Option<T>)
  = set(slot.index(), valueOption) 

override operator fun iterator() = items.iterator()        

The requireValidIndex function simply calls require with a range of 0 until 10 to ensure the index is valid. The get operator is overloaded to support both integer indices and the Slot enum. Similarly, the set operator includes additional overloads for Option<T> to simplify copying objects between slots.

In just 10 lines of code, we’ve created a mutable fixed-size list implementation in Kotlin. Pretty neat, right?

Application

Now let’s explore how this class is applied in practice. To step up the complexity, let's use it to model a short-term memory system. This system remembers up to ten passwords per namespace, enabling easier reuse. The memory can be persisted within the password database, represented as a large byte array.

In the terminology of this password manager:

  • Namespaces are referred to as nests.
  • Passwords are stored in eggs.
  • Password identifiers are called egg ids.

Now that we clarified this, let's move on.

Each nest is equipped with an egg id memory, which is implemented as a subclass of Slots<EncryptedShell>. Since the nests themselves are slotted, the entire memory structure is essentially a Slots<Slots<EncryptedShell>>.

Below is an example of how this nested structure is traversed and populated from the byte array:

private fun retrieveMemory(
  byteArray: ByteArray, offset: Int
) : Pair<Int, MemoryMap> {
  var incrementedOffset = offset
  return Slots<EggIdMemory>().apply {
    slotIterator().forEach { nestSlot ->
      this[nestSlot].set(
        EggIdMemory().apply {
          slotIterator().forEach { slot ->
            val (entry, newOffset) = byteArray
              .asMemoryEntry(incrementedOffset)
            entry.ifPresent { this[slot].set(it) }
            incrementedOffset = newOffset
          }
        },
      )
    }
  }.let { Pair(incrementedOffset, it) }
}        

The nested memory structure leverages the Slots class to represent both nests and their respective egg memories. At the outer level, Slots<EggIdMemory> models the nests, while each nest contains its own egg id memory, implemented as a subclass of Slots<EncryptedShell>.

The retrieveMemory function efficiently populates this structure from a ByteArray and an initial offset. It iterates through each nest slot, initializing an EggIdMemory for that nest.

For each slot within the nest, it reads an entry from the byte array, sets it if present, and updates the offset to track progress through the data. The function makes use of slotIterator() to iterate over the Slot enum constants.

Finally, it returns a pair comprising the final offset and the populated memory map, showcasing how the Slots abstraction simplifies the management of nested, slotted structures.

Sources

LinkedIn's formatting of source code has a lot of room for improvement to put it mildly, so for increased readability you might want to check out the complete Slots class hosted on GitHub.

You can find the retrieveMemory function that showcases its application in the same repository.

What is your favourite Kotlin feature? Have you ever overloaded an operator, in Kotlin or any other language that supports it?

要查看或添加评论,请登录

Christian Pflugradt的更多文章

  • Unit tests matter

    Unit tests matter

    Unit Tests really pay off in complicated domains. My password manager currently has more than 500 tests, and thanks to…

  • Can ChatGPT fix an ArchUnit test?

    Can ChatGPT fix an ArchUnit test?

    ChatGPT is great for many things: writing essays, drafting emails or even whipping up a quick dinner recipe. But what…

    2 条评论
  • Efficient BFS in Rust

    Efficient BFS in Rust

    About a year ago, I discovered Advent of Code, an annual series of coding challenges that commence each December. Eager…

  • Microservices aren't for everyone

    Microservices aren't for everyone

    There are numerous posts circulating on LinkedIn offering software engineering tips, particularly in my news feed. A…

社区洞察