Published on

Kotlin Extension Functions on Specific Generic Types

Authors

Today while completing a ticket I came across some code I was itching to clean up. This code had a fortune of repetition and it felt like it had plenty of low hanging fruit I could sort out. For example one thing this code did was call a service that returns a Map. This maps values were then used in a number of different places but in the same boiler plate, less readable way. For example:

val namesToResults = myAwesomeNameService.doYourThing()
...
val result = namesToResults[someName]?.value.joinToString(",")
             ?: throw SomeException("We do not have $someName in our records")
...
val result2 = namesToResults[someName]?.value.joinToString(";")
              ?: throw SomeException("We do not have $someName in our records")
...
//this sure feels like deja vu
val result3 = namesToResults[someName]?.value.joinToString(",")
              ?: throw SomeException("We do not have $someName in our records")

This is a bit of a contrived example but gives a good feel for the sort of problem I was trying to address. The Java way of doing this would be to make a method for the repeated block and call that instead of the repeated code:

fun concatResults(nameToResults: Map<String,List<String>,someName: String,separator: String): String {
    return namesToResults[someName]?.value.joinToString(",")
            ?: throw SomeException("We do not have $someName in our records")
}

But we can do one better than this in Kotlin using extension functions:

fun Map<String,List<String>.concatResults(someName: String,separator: String = ","): String {
    return this[someName]?.value.joinToString(separator)
            ?: throw SomeException("We do not have $someName in our records")
}

Our original code would then become:

val namesToResults = myAwesomeNameService.doYourThing()
...
val result = namesToResults.concatResults(someName)
...
val result2 = namesToResults.concatResults(someName, ";")
...
//this sure feels like deja vu
val result3 = namesToResults.concatResults(someName)

What is interesting about using extension functions like this is you can create extension functions on a specific collection type. For example:

fun Map<String, Int>.sumValues() = this.values.sum()

fun Map<String, String>.concatValuesUsing(separator: String = ",") = this.values.joinToString(separator)

...
val wordsToNumbers = mapOf("One" to 1, "Two" to 2)
val namesToSurnames = mapOf("John" to "Smith", "Jane" to "Doe")

println(wordsToNumbers.sumValues())
println(namesToSurnames.concatValuesUsing())
println(namesToSurnames.concatValuesUsing("=>"))
//3
//Smith,Doe
//Smith=>Doe

In the above I can use sumValues on wordsToNumbers but if I try use concatValuesUsing on wordsToNumbers I get a compile error. Using extension functions in this way we can make code far more readable by targeting specific types of collections.