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:
val newCircle = circle1.combine(circle2)
You can write:
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:
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:
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:
data class Temperature(val celsius: Double) : Comparable<Temperature> {
override operator fun compareTo(other: Temperature): Int {
return celsius.compareTo(other.celsius)
}
}
Usage:
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:
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:
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:
class Greeter(private val greeting: String) {
operator fun invoke(name: String) {
println("$greeting, $name!")
}
}
Usage:
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 ++
:
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:
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:
class Budget(var amount: Double) {
operator fun plusAssign(expense: Double) {
amount += expense
}
operator fun minusAssign(expense: Double) {
amount -= expense
}
}
Usage:
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:
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:
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:
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:
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
-
Be intuitive - Overloaded operators should behave as users would expect. For instance,
+
should generally be used for addition-like operations, not for removal. -
Respect mathematical properties - When implementing operations like addition, try to respect properties like commutativity and associativity when appropriate.
-
Be consistent - Similar operations should behave consistently across different types.
-
Avoid surprising side effects - Operators should generally be free from side effects that could surprise users.
-
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
-
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.
-
Build a simple Vector3D class with vector operations like dot product, cross product, and magnitude.
-
Implement a Temperature class with different units (Celsius, Fahrenheit, Kelvin) and ensure conversions happen automatically when different temperature units interact.
-
Develop a simple Matrix class supporting addition, subtraction, and matrix multiplication.
-
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! :)