/ KOTLIN, API DESIGN, EXTENSION FUNCTION

Write extension functions for your own classes in Kotlin

A recent question on Kotlin’s Reddit came to my attention lately:

Why is it bad to write extension functions for classes that you own?

— Reddit
https://bit.ly/37RBZTW

One of the answer was that it was the opposite: a good practice, and for several reasons. Among them were two very important ones that improve the design of one’s code:

  1. Able to call the function on a nullable
  2. For a class with generic types, add a function that is only available when a specific type is used e.g. Iterable<Int>.sum()

I completely agree with the fact that it’s a good practice. In this post, I’d like to detail further the benefits of extension functions in those two cases. I assume you’re already familiar with extension functions. If not, please refer to the relevant documentation.

Nullable-friendly

In order to detail the first use-case, let’s consider any function defined in the String class. Here’s how one would call that function on a nullable type:

var myNullableString: String? = null
// Do something on myNullableString
// At this point, myNullableString can be null or not
val capitalized: String? = myNullableString?.capitalize()

Because myNullableString might be null, it’s not valid to directly call the function with the standard . operator: the compiler will flag such code as invalid and prevent compilation. To fix the compilation error, one should use the provided safe call operator ?..

Thanks to the latter, at runtime, if the variable is not null, everything will work out as expected. If it is, capitalized will be null instead of throwing an exception. This might be enough if one accepts to have null values propagated along the flow, but what if not? Wouldn’t it be great if one could provide a default value if the value was null, just as with Java’s Optional.getOrElse()? Java’s approach is to encapsulate the logic in a class. Otherwise, we would need a static function. In Kotlin, we can write top-level functions:

fun orElse(string: String?, default: String) = string ?: default

However, this can also be written in a different way. One must remember that extension functions are just syntactic sugar for static functions. For that reason, it’s possible to write extension functions on nullable types. Let’s rewrite the above functions with that in mind:

fun String?.orElse(default: String) = this ?: default

It’s now safe to call the orElse() extension function on nullable types:

val nonNull: String = myNullableString.orElse("")
Kotlin’s stdlib provides the String?.orEmpty() extension function, which is an opinionated alternative: the default is the empty string. Likewise, there’s an extension function provided for collections: Collection<T>?.orEmpty().

Generic-dependent

Now onto the other benefit of extension functions. Let’s start with a straightforward use-case: compute the average of the age of all items belonging to a collection of Person. With Java’s Stream API, the solution code would look like the following:

val average = persons.stream()
  .mapToInt(Person::getAge)
  .average()

Readers familiar with the Java’s Stream API know that there’s no method average() on the Stream interface. In fact, it doesn’t make any sense to have one - what would be the average of a collection of Person or of String? However, it does make sense to have one on a collection of int. Therefore, Java provides a dedicated Stream class for int types: IntStream. This interface has methods that are not found in its sister interface Stream. As shown above, in order to transform a Stream into an IntStream, one should use the dedicated mapToInt() method.

Stream API simplified hierarchy

However, Kotlin allows extension functions on genericized types. It’s thus straightforward to write such a function instead of creating a specialized interface:

fun Collection<Int>.average() =
  reduce { a, b -> a + b }            (1)
    .toDouble()
    .div(size.toDouble())
1 a and b are of type Int, so it’s possible to accumulate them one by one

Note that Kotlin’s stdlib provides an Iterable<Int>.sum() extension function. The above code can thus be simplified by replacing the custom reduction with it:

fun Collection<Int>.average() =
  sum()
    .toDouble()
    .div(size.toDouble())

Conclusion

Compared to Java, Kotlin provides far more capabilities regarding one’s API design. In particular, extension functions allow very interesting options. On the opposite of being a bad practice, using them on one’s own classes is a great benefit.

Nicolas Fränkel

Nicolas Fränkel

Nicolas Fränkel is a Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Currently working for Hazelcast. Also double as a teacher in universities and higher education schools, a trainer and triples as a book author.

Read More