Effective Kotlin Extra Item: Use operators to increase readability

This is a chapter from the book Effective Kotlin. You can find it on LeanPub or Amazon.

In the previous item I warned about misuse of operator overloading. In this chapter I would like to show their usefulness for improving readability. I gave this suggestion commonly. Actually, a suggestion to use operator it increases readability was one of my most common suggestion during review process. It is until my team and the teams we worked with learned to do that by themselves.

Let's start with a clear example. With operators, we can operate on BigDecimal and BigInteger similarly as on regular numbers.

val netPrice = BigDecimal("10") val tax = BigDecimal("0.23") val currentBalance = BigDecimal("20") val newBalance = currentBalance - netPrice * tax println(newBalance) // 17.70

We can also add duration to time.

val now = ZonedDateTime.now() val duration = Duration.ofDays(1) val sameTimeTomorrow = now + duration

You can compare them with using explicit methods:

val newBalance = currentBalance.minus(netPrice.times(tax)) val sameTimeTomorrow = now.plus(duration)

I hope that the value of using operators here is clear.

All the classes that are Comparable can be also compared with comparison operators (>, <, >= and <=) and with range check (value in min..max). This includes big numbers (BigDecimal, BigInteger) and object used to represent time and duration (Instant, ZonedDateTime, LocalDate, Duration, etc). This is important, because we often operate on those types, and we often need to compare them (I hope it is clear, that if you represent money, you should use BigDecimal instead of Double that might round some numbers and loose precision).

val now = LocalDateTime.now() val start = LocalDate.parse("2021-10-17").atStartOfDay() val end = LocalDate.parse("2021-10-21").atStartOfDay() if(now > start) { /*...*/ } if(now < end) { /*...*/ } if(now in start..end) { /*...*/ } val price = BigDecimal("100.00") val minPrice = BigDecimal("10.00") val maxPrice = BigDecimal("1000.00") if(price > minPrice) { /*...*/ } if(price < maxPrice) { /*...*/ } if(price in minPrice..maxPrice) { /*...*/ }

Those are an alternative to using the following methods:

if(now.isAfter(start)) { /*...*/ } if(now.isBefore(end)) { /*...*/ } if(!now.isBefore(start) && !now.isAfter(end)) { /*...*/ } if(price.compareTo(minPrice) > 0) { /*...*/ } if(price.compareTo(maxPrice) < 0) { /*...*/ } if(minPrice.compareTo(price) <= 0 && price.compareTo(maxPrice) <= 0) { /*...*/ }

Although isAfter and isBefore might be more readable than comparison operators, I hope it seems clear that in other cases' operator are clearly easier to understand.

It is worth noticing, that there is an inconsistency between BigDecimal functions equals and compareTo. Function equals checks the number of decimal places, so BigDecimal("1.0") is not equal to BigDecimal("1.00"). This is something you should consider when you compare two numbers. This is one of the reasons why we tend to use BigDecimal numbers with the same precision in the whole project. The function compareTo does not look at the precision, so is possible that A >= B, A <= B, but A != B (so the contract of compareTo is violated, as explained in Item 44).

val num1 = BigDecimal("1.0") val num2 = BigDecimal("1.00") println(num1 == num2) // false println(num1 >= num2 && num1 <= num2) // true

The last typical case when I introduce an operator is when we need to check if an element is in a collection or a set. The classic way to do that is using contains, but we could as well use an operator in.

val SUPPORTED_TAGS = setOf("ADMIN", "TRAINER", "ATTENDEE") val tag = "ATTENDEE" println(SUPPORTED_TAGS.contains(tag)) // true // or println(tag in SUPPORTED_TAGS) // true

Compare the two above, and think which one is more readable. For me and the colleagues I discussed this matter with, using in increases readability. Often, but not always. It all depends on what is the active element here. Here tag is clearly active, and putting it up-front makes this sentence easier to read. Just like "A soda is in the fridge" is more intuitive than "The fridge contains a soda". Now consider a case when the collection is more important. For instance "A human has livers" seems more intuitive than "Livers are in a human". The same with the following code:

val ADMIN_TAG = "ADMIN" val admins = users.map { user.tags.contains(ADMIN_TAG) } // or val admins = users.map { ADMIN_TAG in user.tags }

For me, using contains in this case makes the code more clear. I understand that some might have a different feeling about it, so please, do not treat is as a hard rule (do not force it on reviews). Writing a really readable code is a great art, and all the rules should be rather treated as suggestions.

Operators can also be added to our own classes, like units of measure, money wrappers, other kinds of numbers, and others.

@JvmInline value class Centimeter(private val value: Double) { operator fun plus(other: Centimeter): Centimeter = Centimeter(value + other.value) operator fun plus(other: Milimeter): Centimeter = Centimeter(value + other.value * 10) // ... }

Those are the most common cases where I introduce Kotlin operators, but clearly there are much more overloaded operators in the standard library.