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)