Diving into Kotlin .apply{} function

cheesycoderjin

Jin Lee

Posted on December 8, 2023

Diving into Kotlin .apply{} function

Background

Since I started using Kotlin for Android development in 2017, I've been using functions like apply, let, also, and run. These functions are super handy and make my code much easier to read. But here's the thing: How exactly do they work? What makes them so special?

High-order functions

According to the Kotlin doc:

A higher-order function is a function that takes functions as parameters, or returns a function.

Higher-order functions aren't something new in the programming world; yes, even in Java, they've been around since version 8, iirc. They promote reusability and conciseness. They've also unlocked easier implementations like .apply{} (the function we're delving into today) as well as famous asynchronous libraries such as Coroutines.

So enough about the background, let's look at some code.

Inside apply.{} function

As I'm writing this, I was using kotlin-stdlib-common-1.9.0

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
Enter fullscreen mode Exit fullscreen mode

There are three components I want to talk about which are:

  • inline keyword
  • contract
  • Function literals with receiver

inline keyword

So, when we use the inline keyword, we are telling the compiler to swap out the call-site with the actual code of the inline function. This little trick is to cut down on the extra work that high-order functions bring during the runtime phase. High-order functions aren't as lightweight as regular functions.

To illustrate how inline keyword work, let's take a look at javabyte code when the keyword is used:

inline fun foo1(block:() -> Unit) {
    print("Starting foo1 now!")
    block()
    print("Finished foo1!")
}

fun foo2(block:() -> Unit) {
    print("Starting foo2 now!")
    block()
    print("Finished foo2!")
}
Enter fullscreen mode Exit fullscreen mode

And the main function that calls this method:

fun main(args: Array<String>) {
    print("Cheesy Coder")
    foo1 {
        print("I am running foo1 now!")
    }
    foo2 {
        print("I am running foo2 now!")
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's look at what the Java bytecode says (I will only display what matters the most since I want to make comparison between these two methods)

public final static main([Ljava/lang/String;)V
// Skipping until interesting part
   L1
    LINENUMBER 2 L1
    LDC "Cheesy Coder"
    ASTORE 1
   L2
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.print (Ljava/lang/Object;)V
   L3
   L4
    LINENUMBER 3 L4
   L5
    ICONST_0
    ISTORE 1
   L6
    LINENUMBER 22 L6
    LDC "Starting foo1 now!"
    ASTORE 2
   L7
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 2
    INVOKEVIRTUAL java/io/PrintStream.print (Ljava/lang/Object;)V
// Skipping until interesting part

   L17
    LINENUMBER 24 L17
    LDC "Finished foo1!"
    ASTORE 2
   L18
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 2
    INVOKEVIRTUAL java/io/PrintStream.print (Ljava/lang/Object;)V
   L19
   L20
    LINENUMBER 25 L20
    NOP
   L21
    LINENUMBER 6 L21
    GETSTATIC MainKt$main$2.INSTANCE : LMainKt$main$2;
    CHECKCAST kotlin/jvm/functions/Function0
    INVOKESTATIC MainKt.foo2 (Lkotlin/jvm/functions/Function0;)V
   L22
    LINENUMBER 9 L22
    RETURN
Enter fullscreen mode Exit fullscreen mode

The Java bytecode appears distinct when the inline keyword is used. Instead of invoking foo1 within the main function, the implementation contents of foo1 have been copied into the main function. Labels L1 and L2 illustrate the loading of the 'Cheesy Coder' string and the use of java/io/PrintStream to print the loaded string. Labels L6 and L7 represent the loading of the 'Starting foo1 now!' string and its subsequent printing. There is no mention of the reference to foo1 method anywhere in this code. However, upon encountering L21, the instruction is to invoke foo2 method.

Not only that but there are more Java bytecode created around foo2:

public final static foo2(Lkotlin/jvm/functions/Function0;)V
// Skipping implementation

final class MainKt$main$2 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {


  // access flags 0x1041
  public synthetic bridge invoke()Ljava/lang/Object;
    ALOAD 0
    INVOKEVIRTUAL MainKt$main$2.invoke ()V
    GETSTATIC kotlin/Unit.INSTANCE : Lkotlin/Unit;
    ARETURN
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x11
  public final invoke()V
   L0
    LINENUMBER 7 L0
    LDC "I am running foo2 now!"
    ASTORE 1

// Skipping implementation
Enter fullscreen mode Exit fullscreen mode

This represents the overhead cost. Not only foo2 method was created but an additional class is also generated for its parameters. It's very interesting to see how the Java bytecode demonstrates the impact of the inline keyword!

Contract

This API reminds me of require or requireNonNull. I have seen these kind of checks in many different codebase and it can sometimes assure developers certain conditions are met.

Unlike above, contract tells information or hints to the compiler so that it can make informed decision how smartcast works or give warning to developers. In this function .apply{}, the contract is called callsInPlace.

According to Kotlin docs:

Specifies that the function parameter lambda is invoked in place.
This contract specifies that:

  • the function lambda can only be invoked during the call of the owner function, and it won't be invoked after that owner function call is completed;
  • (optionally) the function lambda is invoked the amount of times specified by the kind parameter, see the InvocationKind enum for possible values. A function declaring the callsInPlace effect must be inline.

It's a bit difficult to understand so let's use real example and see the behavior. We are going to create very simple methods called foo1 & foo2:

@ExperimentalContracts
inline fun foo1(condition: Boolean, block: () -> Unit) {
    contract { callsInPlace(block, InvocationKind.UNKNOWN) }
    if (condition) {
        block()
    }
}

@ExperimentalContracts
inline fun foo2(condition: Boolean, block: () -> Unit) {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    if (condition) {
        block()
    }
}
Enter fullscreen mode Exit fullscreen mode

I added condition boolean parameter so that it does not take in account of how many times block is called on runtime. Let's test out foo1 first:

@OptIn(ExperimentalContracts::class)
fun main(args: Array<String>) {
    val someInput: Int
    foo1(false) {
        someInput = 10
    }
    print(someInput)
}
Enter fullscreen mode Exit fullscreen mode

The code itself may not be interesting so let me include the screenshot of IDE:
Contract on IDE
Error message on IDE

Because we told the compiler that block() parameter is called unknown amount (could not be called), the compiler thinks that someInput may not be initialized or it's called many times that val cannot be re-assigned to a new value.

What about foo2?

@OptIn(ExperimentalContracts::class)
fun main(args: Array<String>) {
    val someInput: Int
    foo2(false) {
        someInput = 10
    }
    print(someInput)
}
Enter fullscreen mode Exit fullscreen mode

Contract with lint fixed on IDE
It looks like foo2 has no problem even though we know val someInput would not be initialized. What happens if we run this method?

0
Process finished with exit code 0
Enter fullscreen mode Exit fullscreen mode

It appears that the Kotlin compiler might utilize default values to prevent the application from encountering errors or crashing. Typically, this operation might not be feasible, but by using contracts, we can notify the compiler about the state of the called block.

In essence, when the .apply{} function is invoked, the compiler explicitly acknowledges that the block has been called and respects whatever operations occur within the caller.

There are many different contracts so check out if you have some free time.

Function literals with receiver

This is the main part of the .apply{} function. Initially, looking at this function made me scratch my head. However, once we understand extension functions, it becomes really easy to grasp.

Extension functions seamlessly add functionality without requiring modifications to the class.

fun String.printWithCheese() {
    println("$this Cheese. Prefix length ${this.length}")
}

fun main(args: Array<String>) {
    "my".printWithCheese()
}
Enter fullscreen mode Exit fullscreen mode

We can add the class with new methods that we want to implement without altering the source code (as shown in this example with the String class).

By using this knowledge of extension functions, we can observe that the block parameter data type in the apply{} function is T.() -> Unit. This data type translates as "Here is the lambda function that has a receiver type, T." It utilizes the same concept as extension functions, allowing access to and modification of member instances.

If we were to look at .run{} implementation, we can also observe something similar:

@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
Enter fullscreen mode Exit fullscreen mode

The only difference is the return type for this lambda functions, R. .run{} uses result from the block and return it as its return type.

Conclusion

I hope you enjoyed diving into .apply{} method and seeing many different components played out in just one simple call. I am amazed by the growth of Kotlin over the years and hope to cover more Kotlin-related topics in the future.

💖 💪 🙅 🚩
cheesycoderjin
Jin Lee

Posted on December 8, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related