Operator overloading is a form of polymorphism. Some operators change behaviors on different types. The classic example is the operator plus (+). On numeric values, plus is a sum operation and on String is a concatenation. Operator overloading is a useful tool to provide your API with a natural surface. Let's say that we're writing a Time and Date library; it'll be natural to have the plus and minus operators defined on time units. In this article, we'll understand how Operator Overloading works in Kotlin.
This article has been extracted from the book, Functional Kotlin, by Mario Arias and Rivu Chakraborty.
class Wolf(val name:String) {
operator fun plus(wolf: Wolf) = Pack(mapOf(name to this, wolf.name to wolf))
}
class Pack(val members:Map<String, Wolf>)
fun main(args: Array<String>) {
val talbot = Wolf("Talbot")
val northPack: Pack = talbot + Wolf("Big Bertha") // talbot.plus(Wolf("..."))
}
The operator function plus returns a Pack value. To invoke it, you can use the infix operator way (Wolf + Wolf) or the normal way (Wolf.plus(Wolf)).
Something to be aware of about operator overloading in Kotlin—the operators that you can override in Kotlin are limited; you can't create arbitrary operators.
Binary operators receive a parameter (there are exceptions to this rule—invoke and indexed access).
The Pack.plus extension function receives a Wolf parameter and returns a new Pack. Note that MutableMap also has a plus (+) operator:
operator fun Pack.plus(wolf: Wolf) = Pack(this.members.toMutableMap() + (wolf.name to wolf))
val biggerPack = northPack + Wolf("Bad Wolf")
The following table will show you all the possible binary operators that can be overloaded:
Operator
|
Equivalent | Notes |
x + y | x.plus(y) | |
x - y | x.minus(y) | |
x * y | x.times(y) | |
x / y | x.div(y) | |
x % y | x.rem(y) | From Kotlin 1.1, previously mod. |
x..y | x.rangeTo(y) | |
x in y | y.contains(x) | |
x !in y | !y.contains(x) | |
x += y | x.plussAssign(y) | Must return Unit. |
x -= y | x.minusAssign(y) | Must return Unit. |
x *= y | x.timesAssign(y) | Must return Unit. |
x /= y | x.divAssign(y) | Must return Unit. |
x %= y | x.remAssign(y) | From Kotlin 1.1, previously modAssign. Must return Unit. |
x == y | x?.equals(y) ?: (y === null) | Checks for null. |
x != y | !(x?.equals(y) ?: (y === null)) | Checks for null. |
x < y | x.compareTo(y) < 0 | Must return Int. |
x > y | x.compareTo(y) > 0 | Must return Int. |
x <= y | x.compareTo(y) <= 0 | Must return Int. |
x >= y | x.compareTo(y) >= 0 | Must return Int. |
When we introduce lambda functions, we show the definition of Function1:
/** A function that takes 1 argument. */ public interface Function1<in P1, out R> : Function<R> { /** Invokes the function with the specified argument. */ public operator fun invoke(p1: P1): R }
The invoke function is an operator, a curious one. The invoke operator can be called without name.
The class Wolf has an invoke operator:
enum class WolfActions {
SLEEP, WALK, BITE
}
class Wolf(val name:String) {
operator fun invoke(action: WolfActions) = when (action) {
WolfActions.SLEEP -> "$name is sleeping"
WolfActions.WALK -> "$name is walking"
WolfActions.BITE -> "$name is biting"
}
}
fun main(args: Array<String>) {
val talbot = Wolf("Talbot")
talbot(WolfActions.SLEEP) // talbot.invoke(WolfActions.SLEEP)
}
That's why we can call a lambda function directly with parenthesis; we are, indeed, calling the invoke operator.
The following table will show you different declarations of invoke with a number of different arguments:
Operator | Equivalent | Notes |
x() | x.invoke() | |
x(y) | x.invoke(y) | |
x(y1, y2) | x.invoke(y1, y2) | |
x(y1, y2..., yN) | x.invoke(y1, y2..., yN) |
The indexed access operator is the array read and write operations with square brackets ([]), that is used on languages with C-like syntax. In Kotlin, we use the get operators for reading and set for writing.
With the Pack.get operator, we can use Pack as an array:
operator fun Pack.get(name: String) = members[name]!!
val badWolf = biggerPack["Bad Wolf"]
Most of Kotlin data structures have a definition of the get operator, in this case, the Map<K, V> returns a V?.
The following table will show you different declarations of get with a different number of arguments:
Operator | Equivalent | Notes |
x[y] | x.get(y) | |
x[y1, y2] | x.get(y1, y2) | |
x[y1, y2..., yN] | x.get(y1, y2..., yN) |
The set operator has similar syntax:
enum class WolfRelationships {
FRIEND, SIBLING, ENEMY, PARTNER
}
operator fun Wolf.set(relationship: WolfRelationships, wolf: Wolf) {
println("${wolf.name} is my new $relationship")
}
talbot[WolfRelationships.ENEMY] = badWolf
Operator | Equivalent | Notes |
x[y] = z | x.set(y, z) | Return value is ignored |
x[y1, y2] = z | x.set(y1, y2, z) | Return value is ignored |
x[y1, y2..., yN] = z | x.set(y1, y2..., yN, z) | Return value is ignored |
Unary operators don't have parameters and act directly in the dispatcher.
We can add a not operator to the Wolf class:
operator fun Wolf.not() = "$name is angry!!!"
!talbot // talbot.not()
The following table will show you all the possible unary operators that can be overloaded:
Operator
|
Equivalent
|
Notes |
+x | x.unaryPlus() | |
-x | x.unaryMinus() | |
!x | x.not() | |
x++ | x.inc() | Postfix, it must be a call on a var, should return a compatible type with the dispatcher type, shouldn't mutate the dispatcher. |
x-- | x.dec() | Postfix, it must be a call on a var, should return a compatible type with the dispatcher type, shouldn't mutate the dispatcher. |
++x | x.inc() | Prefix, it must be a call on a var, should return a compatible type with the dispatcher type, shouldn't mutate the dispatcher. |
--x | x.dec() | Prefix, it must be a call on a var, should return a compatible type with the dispatcher type, shouldn't mutate the dispatcher. |
Postfix (increment and decrement) returns the original value and then changes the variable with the operator returned value. Prefix returns the operator's returned value and then changes the variable with that value.
Now you know how Operator Overloading works in Kotlin. If you found this article interesting and would like to read more, head on over to get the whole book, Functional Kotlin, by Mario Arias and Rivu Chakraborty.
Extension functions in Kotlin: everything you need to know
Building RESTful web services with Kotlin
Building chat application with Kotlin using Node.js, the powerful Server-side JavaScript platform