Lucy Linder
DERLIN.

Follow

DERLIN.

Follow
Kotlin is `fun` - extension functions

Kotlin is `fun` - extension functions

Lucy Linder's photo
Lucy Linder
ยทMar 20, 2023ยท

4 min read

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!


In this part:

๐Ÿ”– I created this Table of Contents using BitDownToc.

Previous articles in the Series:

  1. Some cool stuff about functions

  2. Kotlin is fun - Function types, lambdas, and higher-order functions


The basics

Consider the following piece of code:

capitalize(trimSpaces(myString))

Wouldn't this be more readable like this?

myString.trimSpaces().capitalize()

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()

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

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

Or by omitting the this receiver:

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

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" })
    }
}

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()

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() { /*...*/ }
package ch.derlin.foo

import ch.derlin.utils.doStuff

listOf("a", "b").doStuff()

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()
}

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())
}

In this case, what would be the output of:

printName(Rectangle()) // 1
printName(Shape())     // 2

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>

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 ๐Ÿ˜‰.

ย 
Share this