Skip to main content

Kotlin Operator Overloading

When building Domain-Specific Languages (DSLs) in Kotlin, operator overloading is one of the most powerful features at your disposal. It allows you to use standard mathematical and other operators with custom types, making your code more expressive, intuitive, and concise.

What is Operator Overloading?

Operator overloading is the ability to give new meanings to standard operators (+, -, *, /, etc.) when they're used with custom types. In Kotlin, this is achieved through special function names that correspond to each operator.

For example, instead of writing:

kotlin
val newCircle = circle1.combine(circle2)

You can write:

kotlin
val newCircle = circle1 + circle2

This makes your code read like natural language and forms the foundation for building expressive DSLs.

Basic Operator Overloading in Kotlin

Let's start with the simplest form of operator overloading - arithmetic operators.

Arithmetic Operators

To overload an arithmetic operator in Kotlin, you define a function with a specific name:

kotlin
data class Vector2D(val x: Int, val y: Int) {
// Overload the + operator
operator fun plus(other: Vector2D): Vector2D {
return Vector2D(x + other.x, y + other.y)
}

// Overload the - operator
operator fun minus(other: Vector2D): Vector2D {
return Vector2D(x - other.x, y - other.y)
}

// Overload the * operator for scalar multiplication
operator fun times(scalar: Int): Vector2D {
return Vector2D(x * scalar, y * scalar)
}
}

Now you can use these vectors naturally:

kotlin
fun main() {
val v1 = Vector2D(2, 3)
val v2 = Vector2D(5, 1)

val sum = v1 + v2 // Vector2D(x=7, y=4)
val difference = v1 - v2 // Vector2D(x=-3, y=2)
val doubled = v1 * 2 // Vector2D(x=4, y=6)

println("Sum: $sum")
println("Difference: $difference")
println("Doubled: $doubled")
}

Output:

Sum: Vector2D(x=7, y=4)
Difference: Vector2D(x=-3, y=2)
Doubled: Vector2D(x=4, y=6)

Comparison Operators

Kotlin allows overloading of comparison operators like >, <, >=, and <= through compareTo function:

kotlin
data class Temperature(val celsius: Double) : Comparable<Temperature> {
override operator fun compareTo(other: Temperature): Int {
return celsius.compareTo(other.celsius)
}
}

Usage:

kotlin
fun main() {
val freezing = Temperature(0.0)
val boiling = Temperature(100.0)
val warm = Temperature(25.0)

println("Is boiling > freezing? ${boiling > freezing}")
println("Is warm < freezing? ${warm < freezing}")
}

Output:

Is boiling > freezing? true
Is warm < freezing? false

Equality Operators

Equality operators (== and !=) in Kotlin are handled by the equals method, which is automatically defined for data classes. You typically don't need to override these for simple classes.

Indexing Operators

You can make your class support indexing with [] by implementing get and set operator functions:

kotlin
class Matrix(private val rows: Int, private val cols: Int) {
private val data = Array(rows * cols) { 0.0 }

operator fun get(row: Int, col: Int): Double {
require(row in 0 until rows && col in 0 until cols) { "Index out of bounds" }
return data[row * cols + col]
}

operator fun set(row: Int, col: Int, value: Double) {
require(row in 0 until rows && col in 0 until cols) { "Index out of bounds" }
data[row * cols + col] = value
}
}

Usage:

kotlin
fun main() {
val matrix = Matrix(2, 2)

matrix[0, 0] = 1.0
matrix[0, 1] = 2.0
matrix[1, 0] = 3.0
matrix[1, 1] = 4.0

println("Matrix[0,0] = ${matrix[0, 0]}")
println("Matrix[1,1] = ${matrix[1, 1]}")
}

Output:

Matrix[0,0] = 1.0
Matrix[1,1] = 4.0

Advanced Operator Overloading

Function Invocation Operator

The function invocation operator () can be overloaded to make an object callable:

kotlin
class Greeter(private val greeting: String) {
operator fun invoke(name: String) {
println("$greeting, $name!")
}
}

Usage:

kotlin
fun main() {
val casualGreeter = Greeter("Hey")
val formalGreeter = Greeter("Good day")

casualGreeter("Alice") // Hey, Alice!
formalGreeter("Sir Bob") // Good day, Sir Bob!
}

Output:

Hey, Alice!
Good day, Sir Bob!

Unary Operators

Kotlin allows overloading unary operators like +, -, !, and ++:

kotlin
data class Point(var x: Int, var y: Int) {
// Unary minus
operator fun unaryMinus(): Point {
return Point(-x, -y)
}

// Increment
operator fun inc(): Point {
return Point(x + 1, y + 1)
}

// Not operator
operator fun not(): Point {
return Point(y, x) // Swaps x and y
}
}

Usage:

kotlin
fun main() {
var point = Point(3, 4)

val negated = -point // Point(x=-3, y=-4)
val swapped = !point // Point(x=4, y=3)
val incremented = point++ // Returns Point(x=3, y=4), then point becomes Point(x=4, y=5)

println("Original: $point")
println("Negated: $negated")
println("Swapped: $swapped")
println("After increment: $point")
}

Output:

Original: Point(x=4, y=5)
Negated: Point(x=-3, y=-4)
Swapped: Point(x=4, y=3)
After increment: Point(x=4, y=5)

Augmented Assignments

Operators like +=, -=, etc., can be overloaded by implementing plusAssign, minusAssign, etc:

kotlin
class Budget(var amount: Double) {
operator fun plusAssign(expense: Double) {
amount += expense
}

operator fun minusAssign(expense: Double) {
amount -= expense
}
}

Usage:

kotlin
fun main() {
val budget = Budget(1000.0)

budget += 250.0 // Add income
println("After income: ${budget.amount}")

budget -= 320.0 // Subtract expense
println("After expense: ${budget.amount}")
}

Output:

After income: 1250.0
After expense: 930.0

Real-World Applications

Building a Time DSL

Let's create a simple DSL for specifying time durations:

kotlin
class TimeDuration(var hours: Int = 0, var minutes: Int = 0, var seconds: Int = 0) {
operator fun plus(other: TimeDuration): TimeDuration {
val totalSeconds = (this.hours * 3600 + this.minutes * 60 + this.seconds) +
(other.hours * 3600 + other.minutes * 60 + other.seconds)

return TimeDuration(
hours = totalSeconds / 3600,
minutes = (totalSeconds % 3600) / 60,
seconds = totalSeconds % 60
)
}

override fun toString(): String {
return "${hours}h ${minutes}m ${seconds}s"
}
}

val Int.hours: TimeDuration
get() = TimeDuration(hours = this)

val Int.minutes: TimeDuration
get() = TimeDuration(minutes = this)

val Int.seconds: TimeDuration
get() = TimeDuration(seconds = this)

Usage:

kotlin
fun main() {
val meetingDuration = 1.hours + 30.minutes
val breakDuration = 15.minutes + 30.seconds
val totalDuration = meetingDuration + breakDuration

println("Meeting duration: $meetingDuration")
println("Break duration: $breakDuration")
println("Total duration: $totalDuration")
}

Output:

Meeting duration: 1h 30m 0s
Break duration: 0h 15m 30s
Total duration: 1h 45m 30s

Simple Math Expression DSL

Let's create a DSL for building mathematical expressions:

kotlin
sealed class Expression {
abstract fun evaluate(): Double
}

class Constant(private val value: Double) : Expression() {
override fun evaluate(): Double = value
}

class Addition(private val left: Expression, private val right: Expression) : Expression() {
override fun evaluate(): Double = left.evaluate() + right.evaluate()
}

class Subtraction(private val left: Expression, private val right: Expression) : Expression() {
override fun evaluate(): Double = left.evaluate() - right.evaluate()
}

class Multiplication(private val left: Expression, private val right: Expression) : Expression() {
override fun evaluate(): Double = left.evaluate() * right.evaluate()
}

operator fun Expression.plus(other: Expression): Expression = Addition(this, other)
operator fun Expression.minus(other: Expression): Expression = Subtraction(this, other)
operator fun Expression.times(other: Expression): Expression = Multiplication(this, other)

Usage:

kotlin
fun main() {
// Create expressions: (3 + 4) * (7 - 2)
val expression = (Constant(3.0) + Constant(4.0)) * (Constant(7.0) - Constant(2.0))

// Evaluate the expression
val result = expression.evaluate()
println("Result: $result")
}

Output:

Result: 35.0

Best Practices for Operator Overloading

  1. Be intuitive - Overloaded operators should behave as users would expect. For instance, + should generally be used for addition-like operations, not for removal.

  2. Respect mathematical properties - When implementing operations like addition, try to respect properties like commutativity and associativity when appropriate.

  3. Be consistent - Similar operations should behave consistently across different types.

  4. Avoid surprising side effects - Operators should generally be free from side effects that could surprise users.

  5. Document clearly - If your operator does something non-standard, document it well.

Summary

Operator overloading in Kotlin gives you powerful tools for creating expressive and intuitive DSLs. By giving custom meanings to standard operators for your classes, you can write code that reads more naturally and is more concise.

We've covered:

  • Basic arithmetic operator overloading
  • Comparison operators
  • Indexing operators
  • Function invocation
  • Unary operators
  • Building real-world DSLs with operator overloading

When used wisely, operator overloading can significantly improve the readability and expressiveness of your code. This is especially valuable when creating domain-specific languages.

Additional Resources

Exercises

  1. Create a Money class that supports addition, subtraction, and multiplication by a scalar. Include a currency property to ensure operations only work with the same currency.

  2. Build a simple Vector3D class with vector operations like dot product, cross product, and magnitude.

  3. Implement a Temperature class with different units (Celsius, Fahrenheit, Kelvin) and ensure conversions happen automatically when different temperature units interact.

  4. Develop a simple Matrix class supporting addition, subtraction, and matrix multiplication.

  5. Create a mathematical expression DSL that handles variables in addition to constants.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)