Everything else is just a tool to model your domain in a way that’s easier to read and reason about, and to produce much shorter code. Interfaces define module boundaries and can be mocked to enable simpler testing. Through classes, objects, and data structures, you can model your domain entities. Enums make a list of limited options crystal clear. Extensions allow you to add functionality to existing types, without having access to their original source code, giving advantage of better logic structure. With generics, you can write universal code, promoting reuse. And overloaded operators give that extra edge to common operations that benefit from succinct, crisp syntax.

Operator overloading

You can customize operators for your custom types, however you cannot create new ones. With infix notation, you can write functions that appear as new operators. There’s also an operation called destructuring, that deserves a special attention.

At the end of the day, it’s all syntax sugar, albeit quite convenient one.

Predefined overloadable operators

Indexing

interface CustomIndexedCollection {
  operator fun get(key: String): Set<Int>
  operator fun set(value: Set<Int>, key: String)
}

class CustomIndexedCollectionImpl: CustomIndexedCollection {

  private val storage: MutableMap<String, Set<Int>> = mutableMapOf()

  override fun get(key: String): Set<Int> = storage[key] ?: emptySet()

  override fun set(value: Set<Int>, key: String) {
    storage[key] = value
  }
}

Unary operators

inc() and dec() are used for both – prefix and postfix operations that are generated by the compiler.

import kotlin.math.abs

data class DataPoint(val value: Int) {
  operator fun unaryPlus(): DataPoint = DataPoint(abs(value))
  operator fun unaryMinus(): DataPoint = DataPoint(-abs(value))
  operator fun not(): DataPoint = DataPoint(-value)

  operator fun inc(): DataPoint = DataPoint(
    if (value >= 0) value + 1 else value - 1
  )
  operator fun dec(): DataPoint = DataPoint(
    when {
      (value > 0) -> value - 1
      (value < 0) -> value + 1
      else -> 0
    }
  )
}

var dataPoint = DataPoint(1)
var postfixDataPoint = dataPoint++
postfixDataPoint.value == 1 && dataPoint.value == 2
postfixDataPoint = dataPoint--
postfixDataPoint.value == 2 && dataPoint.value == 1

var prefixDataPoint = ++dataPoint
prefixDataPoint.value == dataPoint.value && dataPoint.value == 2
prefixDataPoint = --dataPoint
prefixDataPoint.value == dataPoint.value && dataPoint.value == 1

Binary arithmetic operators

data class Vector(val x: Double, val y: Double)

operator fun Vector.plus(other: Vector): Vector = 
  Vector(x + other.x, y + other.y)

// Analogy applies for all operators below:
// augmented assignment (e.g. +=) can be generated from a binary operator
var vector = Vector(1.0, 0.0) + Vector(0.0, 1.0)
vector += Vector(1.0, 1.0)

operator fun Vector.minus(other: Vector): Vector = 
  Vector(x - other.x, y - other.y)

operator fun Vector.times(scalar: Double): Vector = 
  Vector(x * scalar, y * scalar)

operator fun Vector.div(scalar: Double): Vector = 
  Vector(x / scalar, y / scalar)

operator fun Vector.rem(scalar: Int): Vector = 
  Vector(x % scalar, y % scalar)

operator fun Vector.rangeTo(
  other: Vector
): Pair<ClosedRange<Double>, ClosedRange<Double>> = 
  Pair(x..other.x, y..other.y)

operator fun Vector.rangeUntil(
  other: Vector
): Pair<OpenEndRange<Double>, OpenEndRange<Double>> = 
  Pair(x..<other.x, y..<other.y)

data class Point(var x: Double, var y: Double)

operator fun Point.plusAssign(vector: Vector) {
  x += vector.x
  y += vector.y
}

operator fun Point.minusAssign(vector: Vector) {
  x -= vector.x
  y -= vector.y
}

operator fun Point.timesAssign(multiplier: Double) {
  x *= multiplier
  y *= multiplier
}

operator fun Point.divAssign(divisor: Double) {
  x /= divisor
  y /= divisor
}

operator fun Point.remAssign(divisor: Int) {
  x %= divisor
  y %= divisor
}

Equality

class Bounds(val leftTop: Point, val rightBottom: Point) {
  override fun equals(other: Any?): Boolean =
    (other is Bounds) 
    && leftTop == other.leftTop 
    && rightBottom == other.rightBottom

  override fun hashCode(): Int {
    var result = leftTop.hashCode()
    result = 31 * result + rightBottom.hashCode()
    return result
  }
}

Comparison

val Bounds.area: Double
  get() = (rightBottom.x - leftTop.x) * (rightBottom.y - leftTop.y)

operator fun Bounds.compareTo(other: Bounds): Int = area.compareTo(other.area)

Contains

operator fun Bounds.contains(point: Point): Boolean =
  point.x in leftTop.x..rightBottom.x && point.y in leftTop.y..rightBottom.y

Point(1.0, 1.0) in Bounds(Point(0.0, 0.0), Point(2.0, 2.0))
Point(3.0, 3.0) !in Bounds(Point(0.0, 0.0), Point(2.0, 2.0))

Invoke

Makes your object callable like a function.

class Task(private val work: () -> Unit) {
  operator fun invoke() = work()

  operator fun invoke(times: Int) {
    (0..<times).forEach { _ -> work() }
  }
}

val task = Task { /* work */ }
task()
task(2)

[--]