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?
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:
- Able to call the function on a nullable
- 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.
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.