Reading — step 1 of 6
Learn
~3 min readFunctional Patterns
Kotlin lets types overload built-in operators by defining specially-named functions. Used right, this makes domain types feel native; used wrong, it makes code unreadable.
Defining operator functions
Use the operator modifier on functions with the right name and signature:
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
operator fun minus(other: Point): Point {
return Point(x - other.x, y - other.y)
}
operator fun times(scalar: Int): Point {
return Point(x * scalar, y * scalar)
}
operator fun unaryMinus(): Point {
return Point(-x, -y)
}
}
val a = Point(3, 4)
val b = Point(1, 2)
println(a + b) // Point(x=4, y=6)
println(a - b) // Point(x=2, y=2)
println(a * 3) // Point(x=9, y=12)
println(-a) // Point(x=-3, y=-4)
The compiler maps a + b to a.plus(b), a - b to a.minus(b), etc.
The full operator table
| Operator | Function name |
|---|---|
+ | plus |
- | minus |
* | times |
/ | div |
% | rem |
+= | plusAssign |
-= | minusAssign |
== | equals (special — defined on all types) |
<, >, <=, >= | compareTo |
[i] | get(i) |
[i] = v | set(i, v) |
() | invoke |
.. | rangeTo |
in | contains |
Indexed access
class Matrix(val rows: Int, val cols: Int) {
private val data = IntArray(rows * cols)
operator fun get(r: Int, c: Int): Int = data[r * cols + c]
operator fun set(r: Int, c: Int, value: Int) {
data[r * cols + c] = value
}
}
val m = Matrix(3, 3)
m[0, 0] = 5
println(m[0, 0]) // 5
invoke — calling like a function
class Greeter(val greeting: String) {
operator fun invoke(name: String) = "\$greeting, \$name!"
}
val hello = Greeter("Hello")
println(hello("Alice")) // "Hello, Alice!"
Comparison via Comparable
For ordering operators, implement Comparable instead of compareTo directly:
data class Version(val major: Int, val minor: Int) : Comparable<Version> {
override fun compareTo(other: Version): Int {
return compareValuesBy(this, other, Version::major, Version::minor)
}
}
Version(1, 5) < Version(1, 6) // true
When NOT to overload operators
Operator overloading is great when the operator's intuitive meaning matches the operation. It's terrible when it doesn't:
// Good — Vector + Vector is intuitive
operator fun Vector.plus(other: Vector) = Vector(...)
// Bad — User * Permission has no intuitive meaning
operator fun User.times(p: Permission) = grant(this, p)
Readers will assume * does multiplication. Don't surprise them.
Common mistakes
- Forgetting
operatormodifier — function works but isn't picked up for the operator syntax. - Wrong return type —
operator fun plusshould return a meaningful result, often the same type. - Surprising semantics —
==should mean equality,<should mean less than. If it's not intuitive, name it explicitly:Vector.dot()not overloaded*. - Implementing
equalswithouthashCode— they go together. Data classes auto-generate both consistently.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…