Kotlin Operator Overloading – Working by Convention

Kotlin Operator Overloading and Conventions

Introduction

Kotlin supports a technique called conventions, everyone should be familiar with. For example, if you define a special method plus in your class, you can use the + operator by convention: Kotlin Operator Overloading.
In this article, I want to show you which conventions you can use and I will also provide a few Kotlin code examples that demonstrate the concepts.

Kotlin Conventions

Kotlin defines conventions that we can apply by implementing methods that comply with predefined names like plus. This is contrary to how Java does provide equivalent language features, which is by specific classes like Iterable to make objects usable in for loops for example.
This different approach, chosen by Kotlin, is more flexible because you can always extend existing classes by defining your own extension functions on existing types, whereas it is often not possible to extend classes with new implemented interfaces like Iterable. Finally, I’m really starting to love extension functions…​ 🙂

In the following descriptions, I’m going to use a class representing a mathematical fraction. This class will not be complete and also won’t provide mathematically perfect implementations. Just in case, you feel like knowing a much better implementation 😉

Dataclass Fraction
data class Fraction(val numerator: Int, val denominator: Int) {
    val decimal by lazy { numerator.toDouble() / denominator }

    override fun toString() = "$numerator/$denominator"
}

The first revision is really simple: We have a data class with two user-defined properties: numerator and denominator. If we print a Fraction to the console, it’s supposed to look like "2/3", so the toString() is being overridden. Also, for comparing two Fraction instances, a lazy property is included for providing the decimal value of the fraction.

Kotlin Operator Overloading

Arithmetic Operators

Overloading operators enables us to use + in other classes than Int or String, you can use Kotlin’s predefined naming conventions to provide this functionality in any class. Let’s add it to our Fraction and see how it’s done.

Binary plus Operator
data class Fraction(val numerator: Int, val denominator: Int) {
    //...

    operator fun plus(add: Fraction) =
            if (this.denominator == add.denominator){
                Fraction(this.numerator + add.numerator, denominator)
            } else {
                val a = this * add.denominator //(1)
                val b = add * this.denominator
                Fraction(a.numerator + b.numerator, a.denominator)
            }

    operator fun times(num: Int) = Fraction(numerator * num, denominator * num)

}

As you can see here, the plus method is defined as an operator function by using the operator keyword, which is relevant because otherwise, the compiler wouldn’t treat plus as a special function complying with a convention. As a second binary operator, times is implemented and also used in the implementation of plus, as you can see in (1). The statement this * add.denominator is translated to this.times(add.denominator) by the compiler.
Let’s see how the Fraction can be used outside of this class now.

Adding two Fractions
var sum = Fraction(2, 3) + Fraction(3, 2)
println("Sum: ${sum.decimal}")

>> prints "Sum: 2.1666666666666665"

As expected, adding two fractions with + is now possible, as well as using * would be as we also provided a times function. If we wanted to be able to multiply a Fraction with another Fraction, we’d have to overload the times function to accept a Fraction parameter instead of an Int. Thus, it isn’t a problem to provide multiple operator functions with different parameter types.

Besides binary operators, we can also overload unary operators to do something like negating a fraction: -Fraction(2,3) or incrementing/decrementing by using --/++. Please have a look at the documentation to learn how the naming conventions look like for these cases.

Comparison Operators

In addition to arithmetic operators, Kotlin does also enable us to overload comparison operators: ==, >=, < and so on. Thus, these operators can be used for any type, not only primitives as in Java. Let’s see, how these conventions look like.

Equality

In Java, we often don’t want to use == when comparing two instances of the same class because it tests for referential equality. Instead, equals is used which can be overridden in order to have a way to compare objects for structural equality. In Kotlin on the other hand, it’s recommended to use == for structural equality and === for referential equality. This also relies on a convention because == is translated to a call of equals under the hood.
The implementation is automatically available for the Fraction data class. If we wanted to implement it ourselves, we would have to override the following function defined in Any:

public open operator fun equals(other: Any?): Boolean

It’s defined as an operator function as we can see, what makes it usable for == and also !=.

More Comparison

If we want to compare two objects in Java to perform sorting, for example, we implement the Comparable interface with its compareTo method. This is also done in Kotlin, but with much better support and a shorthand syntax. If you implement this method in a class, you can use all the nice operators like <, <=, >, >= out of the box. These operators are translated to appropriate calls of compareTo by the compiler: obj1 > obj2obj1.compareTo(obj2) > 0.
Let’s make Fraction comparable and see how it can be used afterward.

Comparable
data class Fraction(val numerator: Int, val denominator: Int) : Comparable<Fraction> {

    //...
    override fun compareTo(other: Fraction) = decimal.compareTo(other.decimal)
}

The implementation is really straightforward, because we can just compare the floating representation of the fractions to each other. The interface method compareTo in Comparable already defines the operator keyword, which makes it not necessary to add the keyword in the presented implementation. If we just implemented the method, without adding the keyword, it would not be possible to use Fraction with the nice, short syntax described earlier. The next example will present its usage.

Comparing Fractions with Short Syntax
println("3/2 > 2/2: ${Fraction(3, 2) > Fraction(2, 2)}")
println("1/2 <= 2/4: ${Fraction(1, 2) <= Fraction(2, 4)}")
>> prints "3/2 > 2/2: true"
          "1/2 <= 2/4: true"

You can see here, that it’s no problem to use the custom Fraction instances in comparisons we only knew for primitives in Java before. This is made possible by Kotlin’s compiler, which I appreciate a lot.

Collections and Ranges

You might have noticed, while writing code in Kotlin, that you can work with lists and maps as you know it from Java arrays, for instance, using the index operator. On top of that, we can use the in keyword to check, whether an element is part of a collection. This is made possible by conventions, too. Kotlin knows the following operators for collections: set and get for using index operators and contains to enable in.
Let’s try an index operator for our Fraction class; as it doesn’t make sense to allow mutability of instances of fractions, we will only provide the get operator (whereas set works exactly the same).
This time we will use an extension function for the operator because this methodology is really important as you can always extend existing classes with your desired operators.

Operator Extension get
operator fun Fraction.get(ind: Int) =
        when (ind) {
            0 -> numerator
            1 -> denominator
            else -> IllegalArgumentException("Index must be 0 or 1")
        }

As already mentioned, operators can have multiple overloaded versions for the same class, here we use a single integer parameter to receive either the numerator or the denominator of a fraction. It’s very easy again: just define a method complying with the naming convention "get" and mark it with the operator keyword. Now we can access the properties with index syntax:

Indexed Access
var sum = Fraction(2, 3) + Fraction(3, 2)
println("Sum numerator: ${sum[0]}")
>> prints "Sum numerator: 13"

As I told you earlier, Kotlin does also allow to use in for checking the existence of an element inside a collection. Unfortunately, this doesn’t make sense for a Fraction. Let’s alternatively check how Kotlin defines such an operator for collections. In Collections.kt we can find an interface method Collection::contains which is marked as an operator:

public operator fun contains(element: @UnsafeVariance E): Boolean

That’s it. Any subclass will have to provide an implementation and everyone will be able to use code like x in listOf(1,2,3) because lists provide a contains method as they implement Collection.

I hope you get the idea of Kotlin conventions by now. There’s even more to learn, one interesting point is ranges.

Ranges

We can use the .. syntax for creating ranges in Kotlin. This is also made possible by conventions because .. is translated to rangeTo method calls internally. This means whenever you feel like wanting to enable your class to be usable with ranges, just implement an operator with the name "rangeTo" and you’ll be fine. If you already implemented the Comparable interface, this isn’t even necessary, because Kotlin provides a generic extension to all comparable classes:

Comparable rangeTo Extension
operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>

As an example, let’s see how Fraction can be used with ranges.

Ranges
val fracRange = Fraction(1, 5)..Fraction(5, 7)
println(Fraction(3, 5) in fracRange)
>> prints "true"

As we saw earlier, Fraction is-a Comparable class, which enables us to use ranges out of the box.
One more complex task is to make a class iterable over ranges. This means we would like to use a range of fractions inside a for-loop. In Kotlin, every class providing an operator method iterator() can be used in such loops. This also explains, how a String can be used in a for-loop: Kotlin defines an extension method to it complying with the conventions.

Let’s see, how we can make Fraction iterable. As we know from the example Comparable rangeTo Extension, using the rangeTo syntax is going to create an instance of ClosedRange<Fraction>, which, by default, doesn’t have an iterator() function. I’ll show you how to create this method:

Iterator Extension
operator fun ClosedRange<Fraction>.iterator() =
        object : Iterator<Fraction> {
            var curr: Fraction = start
            override fun hasNext() = curr <= endInclusive
            override fun next() = curr++

        }

We just extend ClosedRange<Fraction> with the desired method and create an implementation of Iterator<Fraction> which is produced by this method.
Afterward, we can use such ranges in loops and print every element of it.

for (i in fracRange) {
   println("Next: $i")
}
>> prints "Next: 1/5
           Next: 2/5
           Next: 3/5"

If you’re very intent on my examples, you might have noticed the use of ++ in the Iterator Extension code listing. This is one of the operators that we’ve not seen so far. The implementation is very easy though:

operator fun Fraction.inc() = Fraction(this.numerator + 1, this.denominator)

There you go, a not very reasonable implementation for incrementing fractions with the ++ operator 🙂

Destructing Declarations

Another Kotlin feature that seems like magic, in the beginning, is destructing declarations. It's often presented using maps in for-loops, destructing the entries of a map into key→value pairs. Have a look at this one:

val map = hashMapOf(1 to "single", 2 to "many")
for ((i, s) in map){ // (1)
    println("Entry in map: $i: $s")
}

In (1) you can observe a destructing declaration in action: Entries of the map are destructed in two separate variables i and s. So how does that work actually? You might have guessed it already: conventions.
When we make use of a destructing declaration (v1, v2, …​, vn) the variables v1..vn are initialized by calls to functions with the name component1, component2, componentN. But how does a Map declare those functions? Of course, Kotlin’s stdlib just adds extensions to Map to enable this functionality.

Map Extensions
public inline operator fun <K, V> Map.Entry<K, V>.component1(): K = key
public inline operator fun <K, V> Map.Entry<K, V>.component2(): V = value

The function component1 simply returns the key whereas component2 returns the value, really easy once again. These functions are created automatically for every data class by default. The Fraction class can be used in destructing declarations, too.

val f = Fraction(2, 3)
val (a, b) = f
println("Destructed f to: ($a, $b)")
>> prints "Destructed f to: (2, 3)"

The componentX functions are generated for every property declared in the primary constructor of a data class.

By now, we’ve observed the most interesting Kotlin conventions being arithmetic and comparison operators, collection and range operators and, last but not least, destruction declarations. One last convention we need to consider is the invoke convention. I guess, there are not many use cases for this, but one important one is DSLs. The next chapter will provide the relevant information.

(Delegation also relies on conventions, but this topic is worth a separate post. If you’re interested, have a look at the documentation, maybe you can spot the convention).

Invoke

As I already intimated in one of my last posts on Function Literals with Receiver, Kotlin is designed to enable the creation of great (internal) Domain Specific Languages. Besides function literals with receiver, another very important language feature enabling DSLs is the invoke convention.
To put it simply, this conventions makes objects callable as a function. This feature does not make much sense in most cases, because it can lead to weird code. Anyway, let’s try it in the Fraction class.

invoke Convention
data class Fraction(val numerator: Int, val denominator: Int) : Comparable<Fraction> {
    //...
    operator fun invoke(prefix: String = "") = println(prefix + toString())

}

In this invoke operator function a single parameter of type String is defined. The parameter is used to prefix the toString() representation of a Fraction instance and print it to the console. So, how do we use this functionality?

var f = Fraction(2, 3)
f("My invoke prefix: ")
>> prints "My invoke prefix: 2/3"

A bit weird, but we can now invoke our fraction with a String, which will print some output on the console. The compiler translates this call to the more comprehensive expression f.invoke("My invoke prefix: "). You’re free to choose any signature for your invoke methods, it’s also possible to return values from it.

Did you know, how Kotlin provides function types in a much better way than Java does? Have a look at Functions.kt: When you use lambdas in your Kotlin code, they are compiled to Function instances, which define invoke methods. The following code demonstrates it:

val sum = { x: Int, y: Int -> x + y }
sum.invoke(3, 10)
sum(3, 10)

We define a simple function for adding two Int values. This function can be called in two ways: by just putting the parameters in parentheses after the function name, or by explicitly calling invoke. That’s because sum is a Function under the hood. Awesome, isn’t it? I’m always happy when I understand some of Kotlin’s magic.
As for DSLs, the reason for using invoke is almost the same. It allows providing flexible ways to use an API.

Conclusion

In this article, I intended to present to you how Kotlin makes use of naming conventions for special functions, which allows us to overload operators, destructure composite values into separate variables, use proper function types or make use of ranges. Understanding these concepts is very important in my opinion since it provides many explanations to some mysteries of Kotlin as a language. Also, anyone using the language will find some concepts useful for his code, I bet. Maybe you’ll now be able to simplify some of your existing code bases. From now on, you’ll always notice operator keywords in Kotlin 😉

If you like to have a look at my examples, the code is available in my GitHub repository. Feel free, to give any feedback, I’m always happy to help. Also, If you like, have a look at my Twitter account and follow if you’re interested in more Kotlin stuff 🙂 Thanks a lot.

If you want to read more about Kotlin's beautiful features I recommend the book Kotlin in Action to you and also like to direct you to my other articles 🙂

 


 

8 thoughts on “Kotlin Operator Overloading – Working by Convention

Leave a Reply

Your email address will not be published. Required fields are marked *