CARGO  CULT
PROGRAMMER

Kotlin Basics: Inline Functions

In Kotlin, using a function as a parameter of another function (so called higher-order functions) feels more natural than in Java.
Using lambdas has some disadvantages, though. Since they’re anonymous classes (and therefore, objects), they need memory (and might even add to the overall method count of your app).
To avoid this, we can inline our methods.

fun notInlined(getString: () -> String?) = println(getString())

inline fun inlined(getString: () -> String?) = println(getString())

These two functions do exactly the same thing - printing the result of the getString function. One is inlined and one is not.

If you’d check the decompiled java code, you would see that the methods are completely identical. That’s because the inline keyword is an instruction to the compiler to copy the code into the call-site.
We can see the difference once we actually call the methods.

fun test() {
  var testVar = "Test"

  notInlined { testVar }

  inlined { testVar }
}

The corresponding (decompiled) java code:

public static final void test() {
    final ObjectRef testVar = new ObjectRef();
    testVar.element = "Test Variable";

    notInlined((Function0)(new Function0(0) {
        public Object invoke() {
            return this.invoke();
        }

        @NotNull
        public final String invoke() {
           return (String)testVar.element;
        }
    }));

    //inlined:
    String var3 = (String)testVar.element;
    System.out.println(var3);
}

As I promised, there is no sign of a call to the function called inlined. The notInlined function is called with a Function0 anonymous class as its parameter. To obey the contract of the Function0 interface, the invoke method needs to be implemented. Since the interface is generic, an additional method, a so-called bridge method1 is generated.
By inlining the function, we omitted the creation of an object with two methods and also saved a method call. All these things could impact our App’s performance. On Android, avoiding synthetic methods is crucial, since our maximum number of methods is limited2.

We shouldn’t inline all of our functions, though. An inlined function is copied to the call-site, so if we use the same function multiple times in our code, the code generated by the compiler will grow. It’s therefore reasonable to not inline large functions.
Another drawback is that an inlined function can’t call itself or any other inline function that calls this function (since the function is gone at compile-time). Also, public inline functions don’t have access to private properties or functions.

The fact that the code will be copied to the call-site has another cool3 side effect: non local returns.

Consider this example:

fun notInlined2(block: () -> Unit) = block()

fun test() {
  notInlined2 {
      if(someValue == 4) {
          print("kthx")
          return
      }
      print("good")
  }
  println(" bye")
}

What do you expect this piece of code to do?
It doesn’t compile. A normal return statement is not allowed inside a lambda. If you want to explicitly return from a lambda, you need to use a label:

fun test() {
  notInlined2 {
      if(someValue == 4) {
          print("kthx")
          return@notInlined2  
      }
      print("good")
  }
  println(" bye")
}

This function will print kthx bye if someValue == 4 and good bye otherwise. This might not be what we want or expect. If I have a normal language construct, e.g. a for-loop or an if-block, calling return will return from the outer function as well. We can make our own functions behave like these language constructs by marking them as inline. Since the code is copied to the call-site, a return will actually return from there, and not from the lambda.

inline fun inlined2(block: () -> Unit) = block()

fun test() {
  inlined2 {
      if(someValue == 4) {
          print("kthx")
          return
      }
      print("good")
  }
  println(" bye")
}

What do you think happens now? If someValue == 4, the function will print kthx, and good bye otherwise.
This behavior is really powerful, especially when you want to create a DSL, since it makes your own functions look like built-in language constructs.

We could e.g. create a ruby-esque unless “operator” (actually a function):

inline fun unless(condition: Boolean, block: () -> Unit) {
    if(!condition) block()
}

...

fun test() {
  unless (someValue == 4) {
    //do some work if someValue != 4
    return
  }

  //do some other work if someValue == 4
}

Remember that these are just the basics concering inline functions. Some more advanced topics are the noinline and crossline keywords, as well as reified generics.
Of course you can find more info on the whole topic in the official documentation.


1 For more info on bridge methods see e.g. This StackOverflow question

2 https://developer.android.com/studio/build/multidex.html#about

3 Or weird? You decide! 😉

© 2024 Lovis Möller