article banner (priority)

Effective Kotlin Item 48: Use inline modifier for functions with parameters of functional types

This is a chapter from the book Effective Kotlin. You can find it on LeanPub or Amazon.

You might have noticed that nearly all Kotlin higher-order stdlib functions have an inline modifier.

public inline fun repeat(times: Int, action: (Int) -> Unit) { for (index in 0 until times) { action(index) } } public inline fun <T, R> Iterable<T>.map( transform: (T) -> R ): List<R> { return mapTo( ArrayList<R>(collectionSizeOrDefault(10)), transform ) } public inline fun <T> Iterable<T>.filter( predicate: (T) -> Boolean ): List<T> { return filterTo(ArrayList<T>(), predicate) }

This inline modifier makes the compiler replace all uses of this function with its body during compilation. Also, all calls of function arguments inside repeat are replaced with these functions’ bodies. So, the following repeat function call:

repeat(10) { print(it) }

Will be replaced with the following code during compilation:

for (index in 0 until 10) { print(index) }

This is a significant change compared to how functions are executed normally. In a normal function, execution jumps into this function body, invokes all statements, then jumps back to the place from where the function was invoked. Replacing calls with bodies is a significantly different behavior.

There are a few advantages to this behavior:

  • A type argument can be reified
  • Functions with functional parameters are faster when they are inline
  • Non-local return is allowed

There are also some costs to using this modifier. Let’s review all the advantages and costs of the inline modifier.

A type argument can be reified

Older versions of Java do not have generics. They were added to the Java programming language in 2004 in version J2SE 5.0, but they are still not present in the JVM bytecode, therefore generic types are erased during compilation. For instance, List<Int> compiles to List. This is why we cannot check if an object is List<Int>. We can only check if it is a List.

any is List<Int> // Error any is List<*> // OK

For the same reason, we cannot operate on a type argument:

fun <T> printTypeName() { print(T::class.simpleName) // ERROR }

We can overcome this limitation by making a function inline. Function calls are replaced with this function’s body, so uses of type parameters can be replaced with type arguments using the reified modifier:

inline fun <reified T> printTypeName() { print(T::class.simpleName) } // Usage printTypeName<Int>() // Int printTypeName<Char>() // Char printTypeName<String>() // String

During compilation, the body of printTypeName replaces usages, and the type argument replaces the type parameter:

print(Int::class.simpleName) // Int print(Char::class.simpleName) // Char print(String::class.simpleName) // String

reified is a useful modifier. For instance, it is used in filterIsInstance from the stdlib to filter only elements of a certain type:

class Worker class Manager val employees: List<Any> = listOf(Worker(), Manager(), Worker()) val workers: List<Worker> = employees.filterIsInstance<Worker>()

reified modifier is also used in many libraries and util functions we define ourselves. The example below presents a common implementation of fromJsonOrNull that uses the Gson library. It also presents how the Koin library uses this kind of function to simplify both dependency injection and module declaration.

inline fun <reified T : Any> String.fromJsonOrNull(): T? = try { gson.fromJson(json, T::class.java) } catch (e: JsonSyntaxException) { null } // usage val user: User? = userAsText.fromJsonOrNull() // Koin module declaration val myModule = module { single { Controller(get()) } // get is reified single { BusinessService() } } // Koin injection val service: BusinessService by inject() // inject is reified

Functions with functional parameters are faster when they are inlined

To be more concrete, all functions are slightly faster when they are inlined. There is no need to jump with execution and track the back-stack. This is why small functions that are used very often in the stdlib are often inlined:

inline fun print(message: Any?) { System.out.print(message) }

However, this difference is most likely insignificant when a function does not have any functional parameters. This is why IntelliJ gives this warning:

To understand why functions with functional parameters are typically faster when marked as inline, we first need to understand what the problem is with operating on functions as objects. These kinds of objects, which are created using function literals, need to be held somehow. In Kotlin/JS, this is simple since JavaScript treats functions as first-class citizens, so there are either functions or function references under functional parameters. In Kotlin/JVM, an object needs to be created using either an anonymous JVM class or a normal class. Therefore, the following lambda expression will be compiled to a class.

// kotlin
val lambda: () -> Unit = {
   // code
}

// compiled to JVM equivalent of
Function0<Unit> lambda = new Function0<Unit>() {
   public Unit invoke() {
       // code
   }
};

Notice that this function type is translated to the Function0 type as this is what a function type with no arguments is compiled to in JVM. Functions with more arguments compile to Function1, Function2,Function3, etc.

  • () -> Unit compiles to Function0<Unit>
  • () -> Int compiles to Function0<Int>
  • (Int) -> Int compiles to Function1<Int, Int>
  • (Int, Int) -> Int compiles to Function2<Int, Int, Int>

All these interfaces are generated by the Kotlin compiler. You cannot use them explicitly in Kotlin though because they are generated on demand, so we should use function types instead. However, knowing that function types are just interfaces opens your eyes to some new possibilities. You can, for instance, implement a function type:

class OnClickListener : () -> Unit { override fun invoke() { // ... } }

As illustrated in Item 47: Avoid unnecessary object creation, wrapping the body of a function into an object will slow down the code. This is why the first of the two functions below will be faster:

inline fun repeat(times: Int, action: (Int) -> Unit) { for (index in 0 until times) { action(index) } } fun repeatNoinline(times: Int, action: (Int) -> Unit) { for (index in 0 until times) { action(index) } }

The difference is visible but it is rarely significant in real-life examples. However, if we design our test well, you can see this difference clearly:

@Benchmark fun nothingInline(blackhole: Blackhole) { repeat(100_000_000) { blackhole.consume(it) } } @Benchmark fun nothingNoninline(blackhole: Blackhole) { noinlineRepeat(100_000_000) { blackhole.consume(it) } }

On my computer, the first one takes 189 ms on average, while the second one takes 447 ms on average. This difference stems from the fact that in the first function we only iterate over numbers and call an empty function. In the second function, we call a method that iterates over numbers and calls an object, and this object calls an empty function. All this difference is due to the fact that we use an extra object (Item 47: Avoid unnecessary object creation).

To show a more typical example, let’s say that we have 5,000 products, and we need to sum up the prices of the ones that have been bought. We can do this simply by:

users.filter { it.bought }.sumByDouble { it.price }

On my machine, it takes 38 ms to calculate on average. How much would it be if the filter and sumByDouble functions were not inline? 42 ms on average on my machine. This doesn't look like a lot, but this is around 10% difference every time you use these methods for collection processing.

A more significant difference between inline and non-inline functions manifests itself when we capture local variables in function literals. A captured value needs to be wrapped into some object, and whenever it is used, this needs to happen through this object. For instance, in the following code:

var l = 1L noinlineRepeat(100_000_000) { l += it }

A local variable cannot be used directly in a non-inline lambda. This is why the value of a will be wrapped into a reference object during compilation:

val a = Ref.LongRef() a.element = 1L noinlineRepeat(100_000_000) { a.element = a.element + it }

This is a more significant difference because such objects might be used many times: every time we use a function created by a function literal. For instance, in the above example, we use a twice, therefore the extra object will be used 2 * 100,000,000 times. To see this difference, let’s compare the following functions:

@Benchmark // On average 30 ms fun nothingInline(blackhole: Blackhole) { var l = 0L repeat(100_000_000) { l += it } blackhole.consume(l) } @Benchmark // On average 274 ms fun nothingNoninline(blackhole: Blackhole) { var l = 0L noinlineRepeat(100_000_000) { l += it } blackhole.consume(l) }

On my machine, the first one takes 30 ms, while the second takes 274 ms. This is due to the accumulated effects of the fact that a function is an object and the local variable needs to be wrapped. These objects make tiny barriers that need to be overcome many times, again and again, and this makes a significant difference in the end. Since in most cases we don’t know how functions with parameters of functional types will be used, when we define a utility function with such parameters, for instance for collection processing, it is good practice to make it inline. This is why most extension functions with parameters of functional types in the stdlib are inline.

Non-local return is allowed

The previously defined noinlineRepeat looks much like a control structure. Just compare it with an if expression or a for loop:

if (value != null) { print(value) } for (i in 1..10) { print(i) } repeatNoninline(10) { print(it) }

One significant difference is that a return is not allowed inside:

fun main() { noinlineRepeat(10) { print(it) return // ERROR: Not allowed } }

This is the result of what function literals are compiled to. We cannot return from main if our code is located in another class. There is no such limitation when a function literal is inlined as the code will be located in the main function anyway.

fun main() { repeat(10) { print(it) return // OK } }

Thanks to that, functions can look and behave more like control structures:

fun getSomeMoney(): Money? { repeat(100) { val money = searchForMoney() if (money != null) return money } return null }

Costs of inline modifiers

Inline is a useful modifier, but it should not be used everywhere due to its costs and limitations. Let's review them.

Inline functions cannot be recursive. Otherwise, they would replace their calls infinitely. Recurrent cycles are especially dangerous because, at the moment, they do not show an error in IntelliJ:

inline fun a() { b() } inline fun b() { c() } inline fun c() { a() }

Inline functions cannot use elements that have restricted visibility. We cannot use private or internal functions or properties in public inline functions. We cannot use private properties in public or inline functions.

internal inline fun read() { val reader = Reader() // Error // ... } private class Reader { // ... }

This is why they cannot be used to hide implementation, so they are rarely used in classes.

Inline functions make our code grow. To see the scale of this growth, let’s say that I really like printing 3. I first defined the following function:

inline fun printThree() { print(3) }

I wanted to call it 3 times, so I added this function:

inline fun threePrintThree() { printThree() printThree() printThree() }

I still wasn’t satisfied, so I defined the following functions:

inline fun threeThreePrintThree() { threePrintThree() threePrintThree() threePrintThree() } inline fun threeThreeThreePrintThree() { threeThreePrintThree() threeThreePrintThree() threeThreePrintThree() }

What are they all compiled to? The first two are very readable:

inline fun printThree() { print(3) } inline fun threePrintThree() { print(3) print(3) print(3) }

The next two were compiled to the following functions:

inline fun threeThreePrintThree() { print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) } inline fun threeThreeThreePrintThree() { print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) print(3) }

This is an abstract example, but it shows a big problem with inline functions: code grows really quickly when we overuse them. I have actually encountered this problem in a real-life project. Having too many inline functions calling each other is dangerous because our code might start growing exponentially.

Crossinline and noinline

There are cases in which we want to inline a function, but for some reason, we cannot inline all functions used as arguments. In such cases, we can use the following modifiers:

  • crossinline - this means that the function should be inlined but non-local return is not allowed. We use it when this function is used in another scope where non-local return is not allowed; for instance, in another lambda that is not inlined.

  • noinline - this means that this argument should not be inlined at all. It is used mainly when we use this function as an argument to another function that is not inlined.

inline fun requestNewToken( hasToken: Boolean, crossinline onRefresh: () -> Unit, noinline onGenerate: () -> Unit ) { if (hasToken) { httpCall("get-token", onGenerate) // We must use // noinline to pass function as an argument to a // function that is not inlined } else { httpCall("refresh-token") { onRefresh() // We must use crossinline to // inline function in a context where // non-local return is not allowed onGenerate() } } } fun httpCall(url: String, callback: () -> Unit) { /*...*/ }

It is good to know what the meaning of both modifiers is, but we can live without remembering them as IntelliJ IDEA suggests them when they are needed:

Summary

The main cases in which we use inline functions are:

  • Very frequently used functions, like print.
  • Functions that need to have a reified type passed as a type argument, like filterIsInstance.
  • When we define top-level functions with parameters of functional types, especially helper functions, like collection processing functions (like map, filter, flatMap, joinToString), scope functions (like also, apply, let), or top-level utility functions (like repeat, run, with).

We rarely use inline functions to define an API, and we should be careful when one inline function calls some other inline functions. Remember that code growth accumulates.