Kotlin is `fun` - extension functions

Lucy Linder - Mar 20 '23 - - Dev Community

Kotlin provides the ability to extend a class or an interface with new functionality without having to inherit from the class or use design patterns such as Decorator. This is done via special declarations called extensions.

For example, it is possible to write new functions for a class or an interface from a third-party library that you can't modify, or from built-in types. Such functions can be called in the usual way as if they were methods of the original class. This mechanism is called an extension function.

This is an awesome feature and will shed light on the Kotlin standard library. Read on!


🔖 I created this Table of Contents using BitDownToc. If you are curious, read my article: Finally a clean and easy way to add Table of Contents to dev.to articles 🤩


The basics

Consider the following piece of code:

capitalize(trimSpaces(myString))
Enter fullscreen mode Exit fullscreen mode

Wouldn't this be more readable like this?

myString.trimSpaces().capitalize()
Enter fullscreen mode Exit fullscreen mode

This is exactly what extension functions let you do! Let's implement the example above.

Instead of this dull regular function:

// regular function
fun trimSpaces(s: String): String =
    s.replace("\\s+".toRegex(), " ").trim()
Enter fullscreen mode Exit fullscreen mode

We can define trimSpaces as an extension to the String class like this:

// extension function
fun String.trimSpaces(): String =
   this.replace("\\s+".toRegex(), " ").trim()
Enter fullscreen mode Exit fullscreen mode

Or by omitting the this receiver:

// same extension function,
// but using the implicit receiver (no "this")
fun String.trimSpaces(): String =
   replace("\\s+".toRegex(), " ").trim()
Enter fullscreen mode Exit fullscreen mode

As you can see, an extension function simply prefixes the method name with a receiver type (ReceiverType.methodName), which refers to the type being extended. When called, the method can access the receiver - the instance it is called on - directly or using the this keyword. That's it.

The receiver type can be a built-in type (String, Int, Any, ...), a collection type (List<Int>, Map<String, Map<String, Any>>, ...), a nullable type (String?, Any?), or even a generic type (T).

Long story short, it can be applied to pretty much anything. This makes this concept very powerful and versatile.

Here is another, more complex one:

fun <T> List<T>?.prettyPrint() {
    if (this.isNullOrEmpty()) {
        println("Empty list")
    } else {
        println("List content ($size elements):")
        println(joinToString("\n") { "* $it" })
    }
}
Enter fullscreen mode Exit fullscreen mode

The latter may be called on any list:

// an empty list
emptyList<String>().prettyPrint()

// a list of `Any`
listOf("x", 1).prettyPrint()

// or even a null
val lst: List<Int>? = null
lst.prettyPrint()
Enter fullscreen mode Exit fullscreen mode

For real-life examples, see the end of my text utils in goodreads-metadata-fetcher, or my MiscUtils class (Android) in easypass.

Cherries on the cake, IDEs auto-suggest extensions functions for you, so they pop up on the auto-complete dropdowns 💖.


They are still functions

As extension functions are, in fine, functions, they support the same visibility modifiers as regular functions and can be imported around.

package ch.derlin.utils

fun List<String>.doStuff() { /*...*/ }
Enter fullscreen mode Exit fullscreen mode
package ch.derlin.foo

import ch.derlin.utils.doStuff

listOf("a", "b").doStuff()
Enter fullscreen mode Exit fullscreen mode

They can even be made scope local (declared inside another method), for example:

fun fuzzyCompare(expected: String, actual: String): Boolean {
  // here, this complex chain is used twice in the body
  // instead of copy-pasting, we can write it once ...
  fun String.cleaned() = lowercase()
      .removeDiacritics()
      .removeInitials()
      .replace("[^a-z0-9]".toRegex(), "")
      .replace(" +".toRegex(), " ")
      .trim()

  // ... and use it twice
  return actual.cleaned() == expected.cleaned()
}
Enter fullscreen mode Exit fullscreen mode

A compile-time sugarcoating

It is essential to understand that extensions do not modify the classes they extend. By defining an extension, we are not inserting new members into a class, only making new functions callable with the dot-notation on variables of this type.

More importantly, extension functions are statically resolved. In other words, the magic happens at compile time. This has some consequences.

Consider the following:

open class Shape
class Rectangle: Shape()

fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"

fun printName(s: Shape) {
    println(s.getName())
}
Enter fullscreen mode Exit fullscreen mode

In this case, what would be the output of:

printName(Rectangle()) // 1
printName(Shape())     // 2
Enter fullscreen mode Exit fullscreen mode

Well, this will actually print Shape twice. Why? Because in printName, the parameter is declared as Shape, so even if the parameter is of another type at runtime, at build time it is known only as a shape.

(I ignored it for a very long time and never encountered such a situation, but it is something to keep in the back of your mind.)


The magic of Kotlin's standard library explained

The Kotlin Standard Library makes heavy use of extension functions. Ever had a look at the signature of joinToString(), all { ... } or lines()?

// lines() is actually an extension function working
// on any CharSequence, which is a base class for String
fun CharSequence.lines(): List<String>
Enter fullscreen mode Exit fullscreen mode

They are all extension functions! Just look at their definitions, you'll see :)


There is so much more to say about extension functions. I strongly encourage you to read the kotlin doc on extensions and start playing around with them yourselves!

But beware, it's addictive 😉.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .