Using Kotlin extensions in Groovy
Extensions in Kotlin and Groovy
Kotlin and Groovy have mechanisms for extending existing classes without using inheritance or decorators. In both languages, the mechanisms are called extension methods. Their source code looks different, but generated bytecode is quite similar. Thanks to that, Groovy is able to use Kotlin extensions just like its own.
Why would I want to use such extensions in Groovy? The main reason is that I want to test my extensions using the best testing framework available for the JVM – Spock Framework.
Code is available here.
Extensions in Kotlin
There are many types of extensions in Kotlin. I decided to focus only on extension functions and properties.
As an example, I extend the java.lang.String
class. First, I create an extension function skipFirst
, which skips first N
characters:
fun String.skipFirst(n: Int) = if (length > n) this.substring(n) else ""
Next, I create an extension property answer
, which is the Answer to the Ultimate Question of Life, the Universe, and Everything:
val String.answer get() = 42
Both extensions are declared in package com.github.alien11689.extensions
, in file called StringExtensions
. However, the generated class in target directory is named StringExtensionsKt
and this is the name that must be used when accessing from other languages. Specific class name can be forced by annotation @file:JvmName
.
Using Kotlin extensions in Groovy
There are two ways for using extensions in Groovy that are supported by good IDEs. First, you can declare scope where the extensions are available by use
method:
def "should use extension method"() { expect: use(StringExtensionsKt) { input.skipFirst(n) == expected } where: input | n | expected "abcd" | 3 | "d" "abcd" | 6 | "" "" | 3 | "" } def "should use extension property"() { expect: use(StringExtensionsKt) { "abcd".answer == 42 } }
It is acceptable, but is not very convenient. The second and much better way is to use an extension module definition. The extension module is defined in file org.codehaus.groovy.runtime.ExtensionModule
in directory src/main/resources/META-INF/services/
. The same directory is monitored by ServiceLoader, but the file format is completely different:
moduleName=string-extension-module moduleVersion=1.0.0 extensionClasses=com.github.alien11689.extensions.StringExtensionsKt
The tests look much better now:
def "should use extension method"() { expect: input.skipFirst(n) == expected where: input | n | expected "abcd" | 3 | "d" "abcd" | 6 | "" "" | 3 | "" } def "should use extension property"() { expect: "abcd".answer == 42 }