Kotlin type Extensions

dgoetsch

Devyn Goetsch

Posted on December 14, 2019

Kotlin type Extensions

Assumptions

  • General understanding of kotlin

The kotlin type system is extremely powerful. It provides a wealth of useful tools: data classes, interfaces with default implemetnations, sealed classes, object types, final by default on concrete types - the list goes on.

However, there is one feature that in my mind, that stands out as being overlooked but incredibly valuable - Extension Types.

What is an extension type?

Lets take a simple class as an example:

import java.util.concurrent.LinkedBlockingQueue

class Context<T> {
    private val items: Queue<T> = LinkedBlockingQueue()

    fun add(item: T) {
        items.add(item)
    }

    fun drainAll(): List<T> {
        val result = items.toList()
        items.clear()
        return result
    }
}

Lets say you had some common methods that you had to run on this class, for example a "merge" method.

In java, you would create static methods like so:

fun <T> merge(context1: Context<T>, context2: Context<T>): Context<T> {
    val newContext = Context<T>()
    context1.drainAll().forEach { newContext.add(it) }
    context2.drainAll().forEach { newContext.add(it) }
    return newContext
}

You could then invoke this method like so:

val context1: Context<T>
val context2: Context<T>

merge(context1, context2)

This implementation works, even if it isn't the most elegant it is pragmatic. Its easy to test, and the semantics are clear.

Extension methods allow us to express the exact same code a little bit more elegantly:

fun Context<T>.merge(context: Context<T>): Context<T> {
    val newContext = Context<T>()
    this.drainAll().forEach { newContext.add(it) }
    context.drainAll().forEach { newContext.add(it_) }
    return newContext
}

You now invoke merge on a context object like you would any normal method on the Context<T> class

val context1: Context<T>
val context2: Context<T>

context1.merge(context2)

The function definitionfun Context<T>.merge(...) tells the compiler within this method, the keywordthis references the Context<T> on which merge was invoked.

Extension type implementation and limitations

At compile time, extension types become a static method that takes in an extra preceding type argument for the first type. In fact, in the above example, the static method example is conceptually what the compiler creates from the extension method example.

Extension methods have a few limitations:

  • extension methods cannot access protected or private members
  • extension methods cannot override existing methods
  • extensions can be messy and can make code much much worse if misused.

Extension Functions

Above we saw a method with an extension type. Kotlin takes this one step further by also providing the ability to also apply this same principle to functions.

fun <T> withContext(body: Context<T>.() -> Unit): List<T> {
    val context = Context<T>()
    context.body()
    return context.drainAll()
}
val result: List<String> = withContext<String> {
    add("hello")
    add("goodbye")
}

Just like extension methods, extention functions expose the type on the left side of the function parameter declaration as "this" within the function. The following screenshot shows the IDE identifying this in the block inside of withContext:

IDE extension function

Extension methods on type parameters

The kotline standard library is full of excellent extension methods like the kotlin use function

First, lets naively attempt to implement use:

inline fun <R> Closeable?.uselessUse(block: (Closeable?) -> R): R { ... }

var stream: BufferedInputStream = BufferedInputStream(FileInputStream(File("my-file.txt")))
it.uselessUse { closable: Closeable? ->
    //doesn't know closable is a BufferedInputStream
    false
}

In this implementation, the block loses context of what type of closable uselessUse was called on. This makes it difficult to interact with the Closeable inside of block, which undermines the utility of this method.

inline fun <T : Closeable?, R> T.use(block: (T) -> R): R { ... }

var stream: BufferedInputStream = BufferedInputStream(FileInputStream(File("my-file.txt")))
stream.use { bufferedInputStream: BufferedInputStream ->
    //knows that the closable was a BufferedInputStream
    String(bufferedInputStream.readAllBytes())
}

Notice how the use function extends a type parameter T: Closable? instead of a concrete type Closable?. This allows the block to have context of what type of Closeable is being used, and allows the user to take full advantage of auto-closable functionality.

Okay, so what?

In Java, auto closable was implemented as a language feature in Java 7. It worked, and it provided some syntactic sugar, but it was incredibly limited in scope. With extension functions, the Kotlin standard library provides an implementation for auto closable and describes a reproducible pattern for extending the language itself.

Extension functions are the main mechanism through which kotlin enables DSL implementation. In my next post I'll go into detail about how this can be done.

💖 💪 🙅 🚩
dgoetsch
Devyn Goetsch

Posted on December 14, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related