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:
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.
Top comments (0)