Learn about defining extension with nullable and generic receivers, and discover a common source of confusion with the latter. Finally, a thorough set of exercises to really get extensions down.
In this article, let’s go through some of the ways extensions interact with other Kotlin features, which might not be immediately apparent.
First off — receivers can be nullable:
//sampleStart fun String?.emptyWhenNull(): String = this ?: "" val test: String? = null val result1 = "A".emptyWhenNull() // "A" val result2 = test.emptyWhenNull() // "" val result3 = null.emptyWhenNull() // "" //sampleEnd fun main() { val poem = """ When you're in the labyrinth of code's maze, Kotlin's syntax is the guiding blaze. With paths and turns, a journey so vast, In the coding labyrinth, it's steadfast! """.trimIndent() println(poem) }
Extensions can also be combined with generics, to great effect:
import java.util.Optional //sampleStart // Defines an extension on all types! fun <T> T.asOptional() = Optional.ofNullable(this) val result1 = 3.asOptional() // Works val result2 = null.asOptional() // Also works //sampleEnd fun main() { val poem = """ Kotlin, the weaver in the coding loom, With extension functions, it dispels the gloom. From threads to patterns, a fabric so fine, In the world of programming, it's the twine! """.trimIndent() println(poem) }
In the example above, T
is unbounded, which is equivalent to an upper bound of Any?
. In other words, this extension is defined on nullable types as well.
To prevent that, you can specify an explicit upper bound:
import java.util.Optional //sampleStart fun <T: Any> T.asOptional() = Optional.of(this) // this can never be null val result1 = 3.asOptional() // Works //val result2 = null.asOptional() // Doesn't work //sampleEnd fun main() { val poem = """ In the coding garden, Kotlin's the bloom, With extension functions, it banishes gloom. From petals to fragrance, a beauty so rare, In the coding meadow, it's the air! """.trimIndent() println(poem) }
A common source of confusion with generic receivers is the difference between the following:
//sampleStart val <T : Number> T.integerValuePlusOne_A get() = toInt() + 1 val Number.integerValuePlusOne_B get() = toInt() + 1 val result1 = (3.2).integerValuePlusOne_A // Works val result2 = (3.2).integerValuePlusOne_B // Also works //sampleEnd fun main() { val poem = """ Kotlin, the explorer in code's vast sea, With extension properties, it sails with glee. From waves to horizons, a journey so wide, In the world of development, it's the tide! """.trimIndent() println(poem) }
At first look, both definitions behave the same and there doesn’t seem to be a clear advantage of using generics at all.
The difference is that the generic variant captures the actual type on which it was called, which allows it to be used e.g. in the return value:
//sampleStart fun <T: Number> T.wrapInList_A() = listOf(this) // Return List<T> fun Number.wrapInList_B() = listOf(this) // Return List<Number> val result1 = (3.2).wrapInList_A() // List<Double> val result2 = (3.2).wrapInList_B() // List<Number> //sampleEnd fun main() { val poem = """ When you're in the symphony of code's song, Kotlin's syntax is the melody strong. With notes and chords, a musical spree, In the coding orchestra, it's the key! """.trimIndent() println(poem) }
When you don’t actually need to use the type for anything, there is no difference between using a generic parameter and just directly defining the extension on the upper bound of the generic parameter.
Generics Combined With Nullable Receivers
//sampleStart // Same fun <T> T.myFun1() = "myFun1" // Same fun <T: Any?> T.myFun1() = "myFun1" // Same fun <T: Any> T?.myFun1() = "myFun1" // Same fun <T> T?.myFun1() = "myFun1" // Same, because we're not using T in the return value fun Any?.myFun1() = "myFun1" // NOT same, the receiver is no longer nullable fun <T: Any> T.myFun2() = "myFun2" // Same as previous, because we're not using T in the return value fun Any.myFun2() = "myFun2" //sampleEnd
It’s very unlikely that you’ll ever have to think about this, but if you do, you can check this for reference.
Exercises
This is a slightly modified version of Java code I encountered on a real project, some time ago:
//sampleStart public interface ArgSrc { /** * Retrieves value of specific argument and converts * it to specified type. * * @param type type of value created from parameter * @param name name of parameter * @param index index of parameter * @param aux auxiliary parameter for the conversion (may be * null). The interpretation of the auxiliary value * is dependent on particular conversion algorithm * that coerces argument to given type. * @param def default value used in situations when no such * parameter exists or the parameter is null. * @return value of specified parameter or def when no value * exists, or value is null. */ <T> T get(Class<T> type, String name, String aux, int index, T def); } //sampleEnd
import org.junit.Assert import org.junit.Test import java.util.* class Test { @Test fun testSimpleGet() { val argSrc = object : ArgSrc { override fun <T : Any> get(type: Class<T>, name: String, aux: String?, index: Int, def: T?): T? = def } Assert.assertNull("SimpleGet is not implemented correctly!", argSrc.simpleGet("name")) } @Test fun testAlsoLog() { Assert.assertTrue("AlsoLog is not implemented correctly!", 3.alsoLog() == 3) } @Test fun testMyAlso() { Assert.assertTrue("MyAlso is not implemented correctly!", 3.myAlso { } == 3) } @Test fun testCompose() { fun String.myCapitalize() = replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } val shout = String::myCapitalize compose { "$it!" } Assert.assertTrue("Compose is not implemented correctly!", shout("hello") == "Hello!") } @Test fun testInto() { Assert.assertEquals("Into is not implemented correctly!", execute(), executeChain()) } @Test fun testNullableGenerics() { Assert.assertTrue("ThrowIfNull is not implemented correctly!", (3 as Int?).throwIfNull() is Int) } @Test fun testFlip() { fun repeat(str: String, num: Int) = str.repeat(num) Assert.assertEquals("Flip is not implemented correctly!", repeat("test", 3), ::repeat.flip()(3, "test")) } } //sampleStart interface ArgSrc { fun <T: Any> get(type: Class<T>, name: String, aux: String?, index: Int, def: T?): T? } // --- 1. --- /** * Take a look at ArgSrc.java above (and it's Kotlin equivalent). It has a cumbersome * get() function, and it turns out that in practice, it's almost always called with * aux = null and index = 0. * * Define a ArgSrc.simpleGet extension function that defines appropriate default * values and calls get() on the receiver. Use reified generics to your advantage. * * Spend some time thinking about the way default values are handled. Can you think of a * way you can use nullability and the elvis operator to remove the def parameter * completely? Can you think of another advantage this style has? * * Hint: argSrc.simpleGet("name") ?: throw ArgumentRequiredException("Name is required!") */ fun ArgSrc.simpleGet(): Nothing = TODO() // --- 2. --- /** * Define a function alsoLog() on every type, which prints the receiver using println * and then returns it. Such a function could be useful for debugging nested expressions * without changing the value of the result, e.g.: * * (fun1()+fun2()).alsoLog() * 2 / 17 == (fun1()+fun2()) * 2 / 17 */ fun alsoLog(): Nothing = TODO() // --- 3. --- /** * Generalize the above function to a myAlso function which instead accepts a lambda * that executes the side effect. * * 3.myAlso { println(it + 2) } // prints 5 and returns 3 * * Be sure to declare it as inline! If you don't know why, re-read the lesson on * inline functions. */ fun myAlso(): Nothing = TODO() // --- 4. --- /** * Write an infix 'compose' extension function which implements mathematical * composition, so e.g. the following works * * val shout = String::capitalize compose { "$it!" } // shout("hello") == "HELLO!" * * and (f compose g)(x) is equivalent to g(f(x)). * * Be sure to declare it as inline! If you don't know why, re-read the lesson * on inline functions. */ fun compose(): Nothing = TODO() // --- 5. --- /** * Sometimes, you get into situations where you need to nest multiple functions, * and it's a mess: * sendResponse(formatForFrontEnd(calculateValue())) * * Things would be much nicer if you had some sort of piping operator that would * allow you to write e.g.: * calculateValue() into ::formatForFrontEnd into ::sendResponse * * Implement the 'into' function. Be sure to declare it as inline! If you don't * know why, re-read the lesson on inline functions. */ fun into(): Nothing = TODO() // --- 6. --- fun calculateValue() = 3 fun validate(value: Int) = value > 0 fun formatForFrontEnd(value: Int): String = "{success: $value}" fun sendResponse(response: String) = println(response) /** * Use the myAlso and into functions to refactor the following function body into * a single expression. Note that I don't specifically recommend using this style, * it's just for practice. */ fun execute() { val value = calculateValue() if(!validate(value)) { throw Exception("Invalid value calculated!") } return sendResponse(formatForFrontEnd(value)) } fun executeChain(): Nothing = TODO() // --- 7. --- /** * The purpose of this function is to simulate the !! operator - in other words, * it should throw a NPE when called on null. However, one very important feature * is missing - the resulting type of calling this function on a nullable receiver * is still nullable, e.g.: * val test: Int? = 3 * val result: Int = test.throwIfNull() // This doesn't work! * * Modify the function so the above works as expected. */ fun <T> T.throwIfNull() = this ?: throw NullPointerException() // --- 8. --- /** * Function types are also a type, so you can define extension functions on function * types as well. * * Implement an extension function on all two parameter functions that produces a * new function with its arguments flipped, e.g.: * * fun repeat(str: String, num: Int) = str.repeat(num) * repeat("test", 3) // "testtesttest" * ::repeat.flip()(3, "test") // "testtesttest" * * Can you think of a situation where this could be useful? Hint: What if you had * a function that accepted a lambda and a number, >in that order<? How could you * use 'flip' to your advantage? */ fun flip(): Nothing = TODO() //sampleEnd