[Kotlin Pearls 6] Extensions: The Good, The Bad and The Ugly

How to use Extension Functions and Properties to improve the readability of your code

Uberto Barbini
ProAndroidDev

--

To Extend or Not To Extend?

Extension Functions (and properties) are a new thing for Java developers, they are present in C# since long but they landed in the JVM land for the first time by courtesy of Kotlin.

Working on big Kotlin codebase and browsing through open source Kotlin code, I have noticed some recurring cases where extensions are useful to improve code readability and some cases where they make the code more difficult to understand.

This is also a hot topic of discussions in teams that are starting Kotlin for the first time, so I think there is some value to make a summary here of what it did work well in my experience and what it didn’t.

Please let me know if you disagree or if you have other examples.

A little introduction on what we meant for Extension in Kotlin.

There are two types of Extensions in Kotlin: Extension Functions and Extension Properties.

First, let’s look at what is an Extension Function and how do you declare it in Kotlin.

fun String.tail() = this.substring(1)

fun String.head() = this.substring(0, 1)

I just wrote two functions, one that returns the first character of a String and other that returns the rest of the String.

The important bit here is that we put the name of the function (e.gtail ) after the type on which we want to apply it, String in our case.

Let’s write a test as an example:

@Test
fun `head and tail`() {t+
assertThat("abcde".head()).isEqualTo("a")
assertThat("abcde".tail()).isEqualTo("bcde")
}

Apparently, we just added two methods to the String class! which is something you are not supposed to do.

But if we look at the equivalent Java code (stripped of intrinsics) we can see that is being translated in a static method with a String parameter.

@NotNull
public static final String head(@NotNull String $this$head) {
String var10000 = $this$head.substring(0, 1);
return var10000;
}

(see my previous post to see how to get Java equivalent code from Kotlin)

So is it only syntax sugar? Well, yes.

Still, extensions can do much to improve the readability or make it worse.

The type of an Extension Function is something like:
String.() -> String

The first String is called the receiver of the function.

Ok, we saw that Extension Functions can be applied to any class. But can we do something more generic? Yes, we can!

fun <T> T.foo() = "foo $this"

Once you this foo is in your scope you can call it on any object, including null.
And here are the tests to prove it.

assertThat(123.foo()).isEqualTo("foo 123")
val frank = User(1, "Frank")
assertThat(frank.foo()).isEqualTo("foo User(id=1, name=Frank)")
assertThat(null.foo()).isEqualTo("foo null")

We can also limit the generic parameter (the T that is) to some class and its subclasses:

fun <T: Number> T.doubleIt(): Double = this.toDouble() * 2

Note that since here we are limiting to Number and not Number? the null is not an acceptable receiver.

assertThat(123.doubleIt()).isEqualTo(246.0)
assertThat(123.4.doubleIt()).isEqualTo(246.8)
assertThat(123L.doubleIt()).isEqualTo(246.0)
//assertThat(null.doubleIt()).isEqualTo(null) it won't compile!

So if we want to restrict our previous foo function to everything not nullable we can declare it like this:

fun <T: Any> T.foo() = "foo $this"

Another case where we need to use Extension Functions is the for infix functions.

For example, I don’t like how Kotlin appends nullable Strings. I would expect that null + null == null and null + "A" == "A" but in Kotlin they would be "nullnull" and "nullA" .

So we can define an infix function called ++ (in backticks):

infix fun String?.`++`(s:String?):String? = 
if (this == null) s else if (s == null) this else this + s

And we can verify it works as expected:

assertThat(null `++` null).isNull()
assertThat("A" `++` null).isEqualTo("A")
assertThat(null `++` "B").isEqualTo("B")
assertThat("A" `++` "B").isEqualTo("AB")

The use of infix functions is also very useful for developing internal DSL, as all the things it can be abused.

Let’s see now how Extension Properties work.

As Extension Functions let us adding methods on existing classes, Extension Properties let us adding properties on existing classes. Pretty straightforward.

As you probably know, Kotlin compiler will create properties for us when we access a Java Bean.

public class JavaPerson {
private int age;
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

When we use it from Kotlin all getters and setters have become properties:

val p = JavaPerson()
p.name = "Fred"
p.age = 32

assertThat(p.name).isEqualTo("Fred")
assertThat(p.age).isEqualTo(32)

These properties are mapped directly on the getters and setters as we can see from the bytecode:

L1
LINENUMBER 15 L1
ALOAD 1
LDC "Fred"
INVOKEVIRTUAL com/ubertob/extensions/JavaPerson.setName (Ljava/lang/String;)V

How do can we declare new properties? Let’s add a millis property to Java Date.

var Date.millis: Long
get() = this.getTime()
set(x) = this.setTime(x)

And here is the test with its use:

val d = Date()
d.millis = 1001

assertThat(d.millis ).isEqualTo(1001L)
assertThat(d.millis ).isEqualTo(d.time)

If we look at the bytecode we can see they are based on a static method:

L1
LINENUMBER 26 L1
ALOAD 1
LDC 1001
INVOKESTATIC com/ubertob/extensions/ExtensionPropertiesKt.setMillis (Ljava/util/Date;J)V

Let me show now how extensions can improve your code. FizzBuzz is a common interview question (and English drinking joke). In case you don’t know it, you can read here the whole explanation.

Now first we can check if a number is a Fizz or a Buzz with a property:

val Int.isFizz: Boolean
get() = this % 3 == 0

val Int.isBuzz: Boolean
get() = this % 5 == 0

Using the null concat extension function we defined before, we can put all together, and implement FizzBuzz in one line of code:

fun Int.fizzBuzz(): String = 
"Fizz".takeIf { isFizz } `++` "Buzz".takeIf { isBuzz }
?: toString()

And this is our test:

val res = (1..15).map { it.fizzBuzz()}.joinToString()

val expected = "1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz"

assertThat ( res ).isEqualTo(expected)

As a conclusion, when should we use extensions and when is better to avoid them?
These are my takes. The lists are completely opinionated you may have different sensibilities but they came from code reviewing other people code so I think they have some merits.
As far as I know, there is no official guideline from Jetbrains on when to use extensions but I used Kotlin stdlib as a guideline.

Usage Patterns for Extension Properties

Good uses:

  • to replace setters and getters
  • to map fields on a property with a better name
  • for tiny functions working on a single field (like isFizz)

Not so good uses:

  • just to get rid of parenthesis in DSL :
    For example to transform an Int to a Duration 5.toHours() is more transparent on what is happening than5.hours
  • mapping non-trivial functions that operate on multiple fields

Usage Patterns for Extension Functions

Good uses:

  • Pure functions with one parameter.
    e.g. String.reverse()
  • toXXX type transformer. Transform on type into another with a new instance.
    e.g. Map.toList(), Int.toPrice()
  • asXXX interface changer. Offer the same object or a part of it with another interface.
    e.g. Map.asSequence(), User.asPerson()
  • Fluentify: change return type and combine with lambdas to simply DSL. e.g. T.apply{…}, T.let{…}
  • Infix functions for pure functions with two parameters.
    e.g. A to B, HttpRoute bind {}
  • Add “syntax sugar” to your classes without accessing inner state.
    e.g. User.fullName()
  • Avoid covariance and contravariance problems with generics¹.
    e.g. Iterable.flatMap {…}

These patterns work well both for “free” extensions and for “contexted” extensions. See my post about them here.

Not so good uses:

  • Complex methods referring to IO or singletons.
    e.g. User.saveToDb(), 8080.startHttpServer()
  • Methods that change the global state.
    e.g. “12:45”.setTime()
  • Function with multiple parameters,
    e.g. “Joe”.toUser(“Smith”, “joe@gmail.com”)
  • Specific methods on widely used types.
    e.g. Date.isFredBirthday(), Double.getDiscountedPrice()

The full code for these examples and more is on my GitHub project.

I hope you liked this post, if so please applaud it and follow me on Twitter and Medium.

The feedback so far on this series of posts has been very positive, thanks to everybody commended and shared my posts.

[1]: Let’s say you have a type like this:

class MyContainerClass<in T> {
fun <U> map(f: (T) -> U): MyContainerClass<U>{...}
}

the compiler will raise an exception because T is declared as in but it appears in a out position.
Moving the map function outside the class as an extension will solve the problem:

class MyContainerClass<in T> {
...
}
fun <U, T> MyContainerClass<T>.map(f: (T) -> U):
MyContainerClass<U>{...}

--

--

JVM and Kotlin independent consultant. Passionate about Code Quality and Functional Programming. Author, public speaker and OpenSource contributor.