Introduction

In this post, I would like to explain in detail what inline functions are and what problems they address. Additionally, I will present some practical examples and tips on when it’s appropriate to use inline functions and when to avoid them.

Before we go any further, let’s first understand what inline functions do. The general idea is simple. If you mark a function with the inline modifier, the compiler will copy the body of that function and paste it at every call site.

As a result, this code:

inline fun foo() {
    print("Body of the foo() function")
}

fun main() {
    foo()
}

after compilation will be ultimately equivalent to this:

inline fun foo() {
    print("Body of the foo() function")
}

fun main() {
    print("Body of the foo() function")
}

What happened here is instead of calling foo(), the compiler copied its content and pasted it into the body of the main() function.

Now that we have a basic intuition for inline functions, we can move on to discuss their applications.

Overhead of lambdas and anonymous functions

In Kotlin, functions are first-class citizens. They can be stored in variables or data structures, used as arguments, or returned from other functions. However, this feature has an important implication. Functions in these scenarios are represented as objects, which impose certain runtime penalties.

Let’s illustrate this with an example. Suppose we write a function called takeUntil (which is surprisingly not in the standard library) that would take all the elements from an iterable until a provided predicate is satisfied.

This is what it could look like:

fun <T> Iterable<T>.takeUntil(predicate: (T) -> Boolean): List<T> {  
    val list = ArrayList<T>()  
    for (item in this) {  
        list.add(item)  
        if (predicate(item))  
            break  
    }  
    return list  
}

We define it as an extension function and specify one argument, predicate, which is a function that will control when we stop taking elements.

In Kotlin, functions that take other functions as parameters, or return a function, are called High-order functions.

Let’s see what the decompiled bytecode for this function looks like in Java:

In IntelliJ (or Android Studio), you can get the decompiled bytecode by going to Tools > Kotlin > Show Kotlin Bytecode and then clicking Decompile button to get the Java code.

public static final List takeUntil(@NotNull Iterable $this$takeUntil, @NotNull Function1 predicate)

Notice that our predicate argument has a special Function1 type (this 1 in Function1 corresponds to the number of arguments our lambda accepts), which confirms that lambdas are indeed represented as objects. We must provide an instance of the Function1 interface every time we call the takeUntil function.

Now, let’s write some code to test our new function and see how lambdas that we pass to the takeUntil function are converted to objects of the Function1 type under the hood:

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5, 6, 7)  
    println(numbers.takeUntil { it >= 5 })
}

When executed, this code prints a correct result (a list of numbers from 1 to 5 included), but we’re interested in something else here. Let’s see the decompiled version of the code above:

List numbers = CollectionsKt.listOf(new Integer[]{1, 2, 3, 4, 5, 6, 7});  
List var1 = takeUntil((Iterable)numbers, (Function1)null.INSTANCE);

As you can see, the takeUntil function takes a mysterious (Function1)null.INSTANCE object as an argument. Where does it come from?

Unfortunately, the generated Function1 interface doesn’t reflect in the decompiled Java code, so we must dive deeper - into the bytecode itself.

I will skip all the irrelevant code for our discussion and present only the interesting parts.

First, we can see a class created that implements the Function1 interface.

final class MainKt$main$1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function1

The bytecode for that class is equivalent to the lambda body that we’ve passed to the takeUntil function.

Our main() function is represented like this:

GETSTATIC MainKt$main$1.INSTANCE : LMainKt$main$1;
CHECKCAST kotlin/jvm/functions/Function1
INVOKESTATIC MainKt.takeUntil (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;)Ljava/util/List;

The GETSTATIC instruction gets a class’s static field value, which means we get a singleton here that is ultimately passed to the takeUntil function.

Let’s do a quick experiment to confirm that if we put our code in a loop that runs 1000 times, we still use that singleton, and we don’t create 1000 new objects:

val numbers = listOf(1, 2, 3, 4, 5, 6, 7)  
repeat(1000) {  
    println(numbers.takeUntil { it >= 5 })  
}

After getting the bytecode, we can confirm that the GETSTATIC instruction is still used, so no new objects are being created.

GETSTATIC MainKt$main$1$1.INSTANCE : LMainKt$main$1$1;
CHECKCAST kotlin/jvm/functions/Function1
INVOKESTATIC MainKt.takeUntil (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;)Ljava/util/List;

So far, nothing dramatic. The bytecode contains an additional class implementing the Function1 interface that represents our lambda passed to the takeUntil function. Additionally, a singleton was used, so there is only one instance of that class at runtime.

However, the situation is entirely different when we start capturing variables in lambdas.

Let’s store the value we had in the lambda in a variable called limit and use it instead:

val limit = 5
val numbers = listOf(1, 2, 3, 4, 5, 6, 7)
repeat(1000) {
    println(numbers.takeUntil { it >= limit })
}

And here’s the bytecode representation:

INVOKESPECIAL MainKt$main$1$1.<init> (I)V
CHECKCAST kotlin/jvm/functions/Function1
INVOKESTATIC MainKt.takeUntil (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;)Ljava/util/List;

As you can see, there is no GETSTATIC instruction anymore. Instead, there is INVOKESPECIAL that calls <init>. This line means we are creating a new object of that class. Given that we run this code 1000 times, the same number of new objects will be created at this point.

It’s worth remembering that when we create lambdas that capture closures, there will always be a new object created, as the above experiment proved. However, in most cases, that won’t be a huge problem (unless you deal with a performance-critical application).

Regardless, the solution is to use inline functions. When we mark our takeUntil function as inline, this is the decompiled Java code that we get for the main() function:

List numbers = CollectionsKt.listOf(new Integer[]{1, 2, 3, 4, 5, 6, 7});  
int limit = 5;  
short var2 = 1000;  
  
for(int var3 = 0; var3 < var2; ++var3) {  
   int var5 = false;  
   Iterable $this$takeUntil$iv = (Iterable)numbers;  
   int $i$f$takeUntil = false;  
   ArrayList list$iv = new ArrayList();  
   Iterator var9 = $this$takeUntil$iv.iterator();  
  
   while(var9.hasNext()) {  
      Object item$iv = var9.next();  
      list$iv.add(item$iv);  
      int it = ((Number)item$iv).intValue();  
      int var12 = false;  
      if (it >= limit) {  
         break;  
      }   
    }
    
   List var13 = (List)list$iv;  
   System.out.println(var13);  
}

The content of the takeUntil function has been copied (inlined) here. Our lambda is no longer represented as an object, so we avoid an additional overhead. Instead, it’s been inlined into the if statement (if (it >= limit)).

Better control flow

Another benefit of using inline functions is introducing a better control flow.

Let’s consider a function that is supposed to find a user with a given id or throw an error when there is no such user:

fun getUser(users: List<User>, id: String): User {  
    users.forEach { user ->  
        print("Inspecting user: $user")  
        if (user.id == id) {  
            return user  
        }  
    }  
    error("User with id $id not found.")  
}

We iterate over the list of users and check if the current user is the one we seek. If it is, we immediately return that user from our getUser function. If we reach the end of the list, we throw an error.

Notice that we use a return statement not inside the getUsers function but in a lambda passed to the forEach. This is only possible because the forEach function has been marked with the inline modifier.

Such returns (located in a lambda, but exiting the enclosing function) are called non-local returns.

If we write our own version of forEach that is not inlined and use it instead, returning from a lambda won’t be possible:

// This is the same function as forEach, but without the inline modifier
fun <T> Iterable<T>.noInlineForEach(action: (T) -> Unit) {  
    for (element in this) action(element)  
}
fun getUser(users: List<User>, id: String): User {  
    users.noInlineForEach { user ->  
        print("Inspecting user: $user")  
        if (user.id == id) {  
            return user  
        }  
    }  
    error("User with id $id not found.")  
}

We are getting an error that says “‘return’ is not allowed here”.

The reason why returning with the regular forEach is possible is that when a lambda is inlined, it’s clear that the return statement applies to the top-level function (getUsers) because there are no other function calls made or objects created.

Noinline

If you don’t want all of the lambdas passed to an inline function to be inlined, you can use the noinline modifier.

Consider the following inline function execute that decides which API to call based on whether the user is part of an experiment or not:

inline fun execute(  
    isPartOfExperiment: (User) -> Boolean,  
    callOldApi: () -> Unit,  
    callNewApi: () -> Unit  
) {  
    // ...
    val callApi = if(isPartOfExperiment(user)) callNewApi else callOldApi  
    // ...
}

We want to store the correct lambda in a variable called callApi and call it later at some point, but that’s not possible. We get the “Illegal usage of inline-parameter” error. We can’t store inlined lambdas in variables (or pass them to other functions) because they aren’t objects anymore.

To solve this, we can add noinline modifiers to two of our lambdas, and our code compiles successfully:

inline fun execute(  
    isPartOfExperiment: (User) -> Boolean,  
    noinline callOldApi: () -> Unit,  
    noinline callNewApi: () -> Unit  
) {  
    // ...
    val callApi = if(isPartOfExperiment(user)) callNewApi else callOldApi  
    // ...
}

Crossinline

In some cases, we still want to get all the benefits of using inline functions but without giving the possibility to use a return statement inside lambdas passed as parameters.

To indicate that the lambda parameter of the inline function cannot use non-local returns, we can mark it with the crossinline modifier.

Take a look at our little helper benchmark function that allows us to measure the execution time of a lambda passed as a parameter:

inline fun benchmark(run: () -> Unit): Long {  
    val duration = measureTime {  
        run()  
    }
    return duration.inWholeMilliseconds  
}

Now, for a little experiment, let’s try to run the following code:

fun main() {
    val time = benchmark { return }  
    print(time)
}

It compiles successfully because the benchmark function is marked with the inline modifier, and using non-local returns is possible in such cases.

If we run it, the console won’t print anything. That’s because the return statement returns not from the benchmark function but immediately from the main(), thus ending the program.

If we don’t want to allow code like this, we can use the crossinline modifier, and the main() function won’t even compile. We will get the “‘return’ is not allowed here” error:

inline fun benchmark(crossinline run: () -> Unit): Long {  
    val duration = measureTime {  
        run()  
    }  
    return duration.inWholeMilliseconds  
}

Reified types

Imagine we want to write a function that will return the first item of a given type from a list. For example, we have a listOf(1, 2, "string", 3) and would like to get the first string from that list.

Here’s one approach to writing such a function:

fun <T> List<*>.firstIsInstance(clazz: Class<T>): T? {  
    @Suppress("UNCHECKED_CAST")  
    return firstOrNull { clazz.isInstance(it) } as T?  
}

And that’s what the code to use this function could look like:

val items = listOf(1, 2, "string", 3)
println(items.firstIsInstance(String::class.java))  

This code works, but it’s not pretty. We have to pass that not-so-beautiful String::class.java to the function.

Additionally, inside, we have to use a suppression mechanism and call the parameter clazz, because class is a reserved keyword. A lot of compromises.

As an alternative, we can rewrite the firstIsInstance function as an inline version and utilize reified type parameters:

inline fun <reified T> List<*>.firstIsInstance(): T? {  
    return firstOrNull { it is T } as T?  
}

Marking the type parameter with the reified modifier makes it accessible inside the function. Also, because the function is inlined, we can use normal operators like as or is, without relying on reflection.

With that change, the client code looks much more elegant:

val items = listOf(1, 2, "string", 3)   
println(items.firstIsInstance<String>())

Avoiding inline functions

So far, we’ve discussed the benefits of using inline functions. However, it’s also worth mentioning when it would be better to avoid them.

First, it doesn’t make much sense to add the inline modifier to a function that doesn’t have any lambdas as parameters because the performance gains are unlikely. The JVM already optimizes small functions and inline them automatically.

Furthermore, you can’t use inline functions when hiding implementation details (public inline functions can’t call private functions):

inline fun doSomething() {  
    doSomethingPrivately() // Won't compile.
}  
  
private fun doSomethingPrivately() {  
    print("Private function")  
}

After learning about the benefits of inline functions, it might be tempting to use them everywhere now but resist the temptation.

For example, inlining a large function could dramatically increase the size of the generated bytecode because it’s copied to every call site. For similar reasons, it’s better to avoid inlining recursive calls.

Summary

This was quite a long post, but I hope you learned a lot from it. As much as I love the Kotlin documentation, I think the section dedicated to inline functions could use some work. That was the main motivation behind the decision to cover inline functions extensively here, including some practical examples.

If you have any questions, feel free to leave a comment here or reach out to me on Twitter.