Skip to main content

Kotlin Custom Operators

One of the most powerful features of Kotlin is the ability to create custom operators through operator overloading. When building domain-specific languages (DSLs), custom operators can make your code more expressive, intuitive, and readable. In this tutorial, we'll explore how to define and use custom operators in Kotlin to enhance your DSLs.

Introduction to Custom Operators

In Kotlin, operators like +, -, *, and others are actually functions with special names. By implementing specific functions in your classes, you can overload these operators to give them custom behavior when working with your own types.

This capability is the backbone of many elegant Kotlin DSLs, allowing you to create code that reads almost like natural language.

Basic Operator Overloading

Let's start with some basic operator overloading in Kotlin.

Binary Operators

Binary operators work with two operands. Here's how to implement the plus (+) operator for a custom class:

kotlin
data class Money(val amount: Int, val currency: String) {
operator fun plus(other: Money): Money {
require(currency == other.currency) { "Cannot add different currencies" }
return Money(amount + other.amount, currency)
}
}

fun main() {
val fiveDollars = Money(5, "USD")
val tenDollars = Money(10, "USD")

val fifteenDollars = fiveDollars + tenDollars

println(fifteenDollars) // Output: Money(amount=15, currency=USD)
}

In this example, we've defined a Money class and implemented the plus operator, allowing us to add two Money instances together using the + symbol.

Unary Operators

Unary operators work with a single operand. Here's how to implement the unary minus (-) operator:

kotlin
data class Temperature(val degrees: Double, val scale: String = "Celsius") {
operator fun unaryMinus(): Temperature {
return Temperature(-degrees, scale)
}
}

fun main() {
val temp = Temperature(25.0)
val negatedTemp = -temp

println(temp) // Output: Temperature(degrees=25.0, scale=Celsius)
println(negatedTemp) // Output: Temperature(degrees=-25.0, scale=Celsius)
}

In this example, we can negate a temperature using the - operator.

Common Kotlin Operators and Their Function Names

Here's a reference table of common operators and the function names you need to implement:

OperatorFunction Name
a + ba.plus(b)
a - ba.minus(b)
a * ba.times(b)
a / ba.div(b)
a % ba.rem(b)
a++a.inc()
a--a.dec()
+aa.unaryPlus()
-aa.unaryMinus()
!aa.not()
a == ba.equals(b)
a > ba.compareTo(b) > 0
a[i]a.get(i)
a[i] = ba.set(i, b)
a()a.invoke()

Building a DSL with Custom Operators

Now let's see how custom operators can be used to build a more complex DSL. We'll create a simple time calculation DSL:

kotlin
class TimeUnit(val value: Int) {
infix fun hours(dummy: Unit) = Duration(value, 0, 0)
infix fun minutes(dummy: Unit) = Duration(0, value, 0)
infix fun seconds(dummy: Unit) = Duration(0, 0, value)
}

data class Duration(val hours: Int, val minutes: Int, val seconds: Int) {
operator fun plus(other: Duration): Duration {
var totalSeconds = (this.seconds + other.seconds) % 60
var carryMinutes = (this.seconds + other.seconds) / 60

var totalMinutes = (this.minutes + other.minutes + carryMinutes) % 60
var carryHours = (this.minutes + other.minutes + carryMinutes) / 60

val totalHours = this.hours + other.hours + carryHours

return Duration(totalHours, totalMinutes, totalSeconds)
}

override fun toString(): String {
return "$hours hours, $minutes minutes, $seconds seconds"
}
}

val Int.time get() = TimeUnit(this)

fun main() {
val duration1 = 2.time hours Unit + 30.time minutes Unit
val duration2 = 1.time hours Unit + 45.time minutes Unit + 15.time seconds Unit

val totalDuration = duration1 + duration2

println("Duration 1: $duration1")
println("Duration 2: $duration2")
println("Total Duration: $totalDuration")
}

Output:

Duration 1: 2 hours, 30 minutes, 0 seconds
Duration 2: 1 hours, 45 minutes, 15 seconds
Total Duration: 4 hours, 15 minutes, 15 seconds

This DSL allows us to write time expressions in a very readable way. The Int.time extension property, along with the infix functions and operator overloading, creates a fluent interface for working with time durations.

Using Invoke Operator for Function-Like Objects

The invoke operator allows objects to be called like functions, which can be powerful in DSLs:

kotlin
class HtmlTag(val name: String) {
private val children = mutableListOf<HtmlTag>()
private val attributes = mutableMapOf<String, String>()

operator fun invoke(setup: HtmlTag.() -> Unit): HtmlTag {
this.setup()
return this
}

infix fun String.equals(value: String) {
attributes[this] = value
}

operator fun String.unaryPlus() {
children.add(HtmlTag("text").apply { attributes["content"] = this@unaryPlus })
}

operator fun HtmlTag.unaryPlus() {
children.add(this)
}

override fun toString(): String {
val attributeString = if (attributes.isEmpty()) "" else
attributes.entries.joinToString(" ", " ") { (k, v) -> "$k=\"$v\"" }

return if (name == "text") {
attributes["content"] ?: ""
} else if (children.isEmpty()) {
"<$name$attributeString/>"
} else {
val childrenString = children.joinToString("")
"<$name$attributeString>$childrenString</$name>"
}
}
}

fun html(setup: HtmlTag.() -> Unit): HtmlTag = HtmlTag("html").apply(setup)
fun HtmlTag.body(setup: HtmlTag.() -> Unit): HtmlTag = HtmlTag("body").apply(setup).also { +it }
fun HtmlTag.div(setup: HtmlTag.() -> Unit): HtmlTag = HtmlTag("div").apply(setup).also { +it }
fun HtmlTag.p(setup: HtmlTag.() -> Unit): HtmlTag = HtmlTag("p").apply(setup).also { +it }

fun main() {
val htmlContent = html {
"lang" equals "en"

body {
div {
"class" equals "container"

p {
+"Hello, this is a paragraph!"
}

p {
+"Another paragraph here."
}
}
}
}

println(htmlContent)
}

Output:

<html lang="en"><body><div class="container"><p>Hello, this is a paragraph!</p><p>Another paragraph here.</p></div></body></html>

In this example:

  1. We use the invoke operator to make our HtmlTag class callable as a function.
  2. We use the unary plus operator (+) to add text and child tags to a parent tag.
  3. The equals infix function sets attributes on tags.

This results in a clean, readable HTML-building DSL.

Indexed Access Operators

The get and set operators allow you to implement indexed access syntax ([]):

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

operator fun get(row: Int, col: Int): Int {
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: Int) {
require(row in 0 until rows && col in 0 until cols) { "Index out of bounds" }
data[row * cols + col] = value
}

override fun toString(): String {
return (0 until rows).joinToString("\n") { row ->
(0 until cols).joinToString(" ") { col ->
this[row, col].toString().padStart(3)
}
}
}
}

fun main() {
val matrix = Matrix(3, 3)

// Use the set operator
matrix[0, 0] = 1
matrix[0, 1] = 2
matrix[0, 2] = 3
matrix[1, 1] = 4
matrix[2, 2] = 5

// Use the get operator
println("Value at [1, 1]: ${matrix[1, 1]}")

// Print the whole matrix
println(matrix)
}

Output:

Value at [1, 1]: 4
1 2 3
0 4 0
0 0 5

This example uses custom indexing operators to create a simple matrix class that can be accessed using matrix[row, col] syntax.

Best Practices for Custom Operators

When using custom operators in your Kotlin DSLs, follow these best practices:

  1. Keep it intuitive: Use operators that make sense for your domain. Don't redefine operators to do something unexpected.

  2. Document well: Since operators can be cryptic, document what each overloaded operator does.

  3. Maintain consistency: Ensure operators behave consistently with other similar operators.

  4. Don't overuse: Just because you can overload operators doesn't mean you should. Use them only when they genuinely improve readability.

  5. Consider alternatives: Sometimes extension functions or infix functions may be a clearer choice than operator overloading.

Common Pitfalls

  1. Precedence issues: Remember that operator precedence is fixed in Kotlin, so your custom operators inherit the precedence of the underlying operator.

  2. Side effects: Avoid side effects in operator functions, as they're expected to be pure operations.

  3. Performance: Be mindful of creating unnecessary objects in operator implementations.

Summary

Custom operators are a powerful feature in Kotlin that allows you to create expressive DSLs. By overloading operators, you can make your code more intuitive and domain-specific. We've covered:

  • Basic operator overloading for binary and unary operators
  • Creating a time calculation DSL using custom operators
  • Using the invoke operator for function-like objects
  • Implementing indexed access with get and set
  • Best practices and common pitfalls

When used judiciously, custom operators can transform your Kotlin code into a clean, readable domain-specific language that's a joy to use.

Exercises

  1. Create a Vector2D class that supports addition, subtraction, and scalar multiplication.

  2. Extend the HTML DSL with additional tags and attributes.

  3. Implement a Money class that handles different currencies and exchange rates.

  4. Create a simple mathematical expression DSL that supports basic operations and variables.

  5. Build a scheduling DSL that uses custom operators to express time intervals and constraints.

Additional Resources

By mastering custom operators, you'll be able to create more expressive and intuitive DSLs in Kotlin, making your code not just functional, but also a pleasure to read and work with.



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)