When should one prefer Kotlin extension functions?

KotlinCode Structure

Kotlin Problem Overview


In Kotlin, a function with at least one argument can be defined either as a regular non-member function or as an extension function with one argument being a receiver.

As to the scoping, there seems to be no difference: both can be declared inside or outside classes and other functions, and both can or cannot have visibility modifiers equally.

Language reference seems not to recommend using regular functions or extension functions for different situations.

So, my question is: when do extension functions give advantage over regular non-member ones? And when regular ones over extensions?

foo.bar(baz, baq) vs bar(foo, baz, baq).

Is it just a hint of a function semantics (receiver is definitely in focus) or are there cases when using extensions functions makes code much cleaner or opens up opportunities?

Kotlin Solutions


Solution 1 - Kotlin

Extension functions are useful in a few cases, and mandatory in others:

Idiomatic Cases:

  1. When you want to enhance, extend or change an existing API. An extension function is the idiomatic way to change a class by adding new functionality. You can add extension functions and extension properties. See an example in the Jackson-Kotlin Module for adding methods to the ObjectMapper class simplifying the handling of TypeReference and generics.

  2. Adding null safety to new or existing methods that cannot be called on a null. For example the extension function for String of String?.isNullOrBlank() allows you to use that function even on a null String without having to do your own null check first. The function itself does the check before calling internal functions. See documentation for extensions with Nullable Receiver

Mandatory Cases:

  1. When you want an inline default function for an interface, you must use an extension function to add it to the interface because you cannot do so within the interface declaration (inlined functions must be final which is not currently allowed within an interface). This is useful when you need inline reified functions, for example this code from Injekt

  2. When you want to add for (item in collection) { ... } support to a class that does not currently support that usage. You can add an iterator() extension method that follows the rules described in the for loops documentation -- even the returned iterator-like object can use extensions to satisfy the rules of providing next() and hasNext().

  3. Adding operators to existing classes such as + and * (specialization of #1 but you can't do this in any other way, so is mandatory). See documentation for operator overloading

Optional Cases:

  1. You want to control the scoping of when something is visible to a caller, so you extend the class only in the context in which you will allow the call to be visible. This is optional because you could just allow the extensions to be seen always. see answer in other SO question for scoping extension functions

  2. You have an interface that you want to simplify the required implementation, while still allowing more easy helper functions for the user. You can optionally add default methods for the interface to help, or use extension functions to add the non-expected-to-be-implemented parts of the interface. One allows overriding of the defaults, the other does not (except for precedence of extensions vs. members).

  3. When you want to relate functions to a category of functionality; extension functions use their receiver class as a place from which to find them. Their name space becomes the class (or classes) from which they can be triggered. Whereas top-level functions will be harder to find, and will fill up the global name space in IDE code completion dialogs. You can also fix existing library name space issues. For example, in Java 7 you have the Path class and it is difficult to find the Files.exist(path) method because it is name spaced oddly. The function could be placed directly on Path.exists() instead. (@kirill)

Precedence Rules:

When extending existing classes, keep the precedence rules in mind. They are described in KT-10806 as:

> For each implicit receiver on current context we try members, then local extension functions(also parameters which have extension function type), then non-local extensions.

Solution 2 - Kotlin

Extension functions play really well with the safe call operator ?.. If you expect that the argument of the function will sometimes be null, instead of early returning, make it the receiver of an extension function.

Ordinary function:

fun nullableSubstring(s: String?, from: Int, to: Int): String? {
    if (s == null) {
        return null
    }

    return s.substring(from, to)
}

Extension function:

fun String.extensionSubstring(from: Int, to: Int) = substring(from, to)

Call site:

fun main(args: Array<String>) {
    val s: String? = null

    val maybeSubstring = nullableSubstring(s, 0, 1)
    val alsoMaybeSubstring = s?.extensionSubstring(0, 1)

As you can see, both do the same thing, however the extension function is shorter and on the call site, it's immediately clear that the result will be nullable.

Solution 3 - Kotlin

There is at least one case where extension functions are a must - call chaining, also known as "fluent style":

foo.doX().doY().doZ()

Suppose you want to extend the Stream interface from Java 8 with you own operations. Of course, you can use ordinary functions for that, but it will look ugly as hell:

doZ(doY(doX(someStream())))

Clearly, you want to use extension functions for that. Also, you cannot make ordinary functions infix, but you can do it with extension functions:

infix fun <A, B, C> ((A) -> B).`|`(f: (B) -> C): (A) -> C = { a -> f(this(a)) }

@Test
fun pipe() {
    val mul2 = { x: Int -> x * 2 }
    val add1 = { x: Int -> x + 1 }
    assertEquals("7", (mul2 `|` add1 `|` Any::toString)(3))
}

Solution 4 - Kotlin

There are cases where you have to use extension methods. E.g. if you have some list implementation MyList<T>, you can write an extension method like

fun Int MyList<Int>.sum() { ... }

It is impossible to write this as a "normal" method.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionhotkeyView Question on Stackoverflow
Solution 1 - KotlinJayson MinardView Answer on Stackoverflow
Solution 2 - KotlinKirill RakhmanView Answer on Stackoverflow
Solution 3 - KotlinbytefuView Answer on Stackoverflow
Solution 4 - KotlinLandeiView Answer on Stackoverflow