Art and science of writing good code

Art and science of writing good code

A Practical Guide to Kotlin's inline Modifier

A Practical Guide to Kotlin's inline Modifier

Featured on Hashnode

Subscribe to my newsletter and never miss my upcoming articles

I fell in love with Kotlin as soon as I switched to it from Java about 4 years ago. And although I was writing Kotlin code on a regular basis ever since, I didn't start using some of its more advanced features until quite recently.

I wish I had started playing around with the more idiomatic ways to write Kotlin code much sooner since it improves the quality of the code and makes writing code much more enjoyable. I agree that you can easily get by without ever using the less-known Kotlin features, however, in the hands of more dedicated software enthusiasts, those features can help improve the code dramatically.

In this article, we will go through one of those features - the inline modifier. And yes, it means we will also take a close look at the modifiers that are closely linked with the inline functions - noinline, crossinline, and reified.

Contents

1. Inline modifier
   1.1. Inline functions
   1.2. Inline properties
   1.3. Inline classes
2. Noinline modifier
3. Crossinline modifier
   3.1. Understanding non-local returns
   3.2. The problem and the crossinline solution
4. Reified modifier

1. Inline modifier

1.1. Inline functions

In Kotlin, you cannot throw a stone without hitting a higher-order function. In layman's terms, those are functions that either take functions as parameters or return a function. This makes Kotlin a very flexible language and allows for intuitive functional programming as well as easy creation of DSLs among many other things.

But as with most amazing things in life, there is a catch. Using many higher-order functions can cause a significant performance impact during runtime since every function compiles to an object that captures a closure. A closure is a fancy way of describing the variables that this function accesses but are declared outside of its scope. Therefore, without a clever solution from the folk at JetBrains, Kotlin might as well have been called the runtime overhead language.

That solution is the inline modifier.

When a function is marked with inline the compiler doesn't create new objects but rather inserts the given code where an inline function is called.

Let's take a look at a simple example:

fun main() {
    doubleAction(
        { println("Hello") },
        { println("World!") }
    )
}

fun doubleAction(
    action1: () -> Unit,
    action2: () -> Unit
) {
    action1()
    action2()
}

If we decompile the bytecode of this block back to Java and look at the main() function, we will see:

public static final void main() {
    doubleAction((Function0)null.INSTANCE, (Function0)null.INSTANCE);
}

To be honest, at first glance this looks like someone is looking for a creative way to get a NullPointerException but let's not concern ourselves with the ways of Kotlin bytecode decompiler.

What we should note here is that our main is calling the doubleAction function and our two lambdas are now instances of Function0 object.

Now, let's see what happens if we put the inline modifier in front of the doubleAction function:

inline fun doubleAction(
    action1: () -> Unit,
    action2: () -> Unit
) {
    action1()
    action2()
}

Just by doing that, after compiling this code into bytecode and decompiling back to Java (and a little bit of cleanup to illustrate the point) the body of our main function reads:

public static final void main() {
    String var1 = "Hello";
    System.out.println(var1);
    var1 = "World!";
    System.out.println(var1);
}

Now that is much more efficient! The compiler even reuses the String variable.

I hope that this example nicely illustrates the benefits of using the inline modifier for higher-order functions. It will inline the function itself as well as lambdas that are passed as arguments.

In summary

The inline modifier provides a noticeable performance boost if you write a lot of higher-order functions, especially in a big codebase, so use it!

Also, for those of you writing Android UI with Jetpack Compose, inline will become your friend pretty fast.

However, a word of caution must be mentioned here. It is not recommended to inline large functions, since it can considerably bloat the generated code. Inlining, like everything else really, is good in moderation.

It works great with short functions, especially when using loops and working with collections.

For example, in Kotlin standard library the scope functions (let, apply, also, run, and with) are marked with inline, as well as extension functions that work with Iterable collections, such as map, forEach, fold and many others.

1.2. Inline properties

Marking higher-order functions is not the only use of the inline modifier, however. One of the less-known features of the Kotlin language are inline properties.

Let's take a look at the following example:

var complexProperty: Int
    get() {
        val x = 2
        val y = 4
        return x + y
    }
    set(value) {
        val adjustedValue = value + 10
        println("Setting adjusted value $adjustedValue")
    }

fun main() {
    complexProperty = 4
    val calculatedProperty = complexProperty
    println("The property is $calculatedProperty")
}

Take note that these are not regular getter and setter methods that read/write a backing field. We will discuss it in a bit.

The accessors of Kotlin properties are compiled to regular methods, which then are used throughout the code. So our example after the bytecode magic will read:

public static final void main() {
    setComplexProperty(4);
    int calculatedProperty = getComplexProperty();
    String var1 = "The property is " + calculatedProperty;
    System.out.println(var1);
}

If you have paid attention until now, you probably know what we are about to do.

Yes, we can mark the property as inline, which will make the accessors of the property work like regular inline functions, even though they are not higher-order.

So after marking our property with inline:

inline var complexProperty: Int
    get() {
        val x = 2
        val y = 4
        return x + y
    }
    set(value) {
        val adjustedValue = value + 10
        println("Setting adjusted value $adjustedValue")
    }

We will get:

public static final void main() {
    // Setter
    int value$iv = 4;
    int adjustedValue$iv = value$iv + 10;
    String var3 = "Setting adjusted value " + adjustedValue$iv;
    System.out.println(var3);

    // Getter
    int x$iv = 2;
    int y$iv = 4;
    int calculatedProperty = x$iv + y$iv;
    String var6 = "The property is " + calculatedProperty;
    System.out.println(var6);
}

Exactly what we would expect from an inlined function.

We can also mark individual accessors as inline:

var complexProperty: Int
    inline get() {
        ...
    }
    set(value) {
        ...
    }

Or:

var complexProperty: Int
    get() {
        ...
    }
    inline set(value) {
        ...
    }

And of course, have read-only properties:

inline val complexProperty: Int
    get() {
       ...
    }

Inline property restriction

There is a reason why we didn't use a property with regular getter and setter in our example. That reason is that the inline modifier is only allowed for accessors that don't have a backing field.

Backing field is an identifier field that doesn't allow us to go into an infinite loop when setting a property.

1.3. Inline classes

Inline classes are another less-known feature of Kotlin that became stable in Kotlin 1.5.0, and even though they are not using the inline modifier directly in their stable version, they did during the experimental phase in earlier Koltin versions, and their functionality is the same as we have seen with previous inlined examples.

Inline classes act as a wrapper around a type to improve readability and decrease the probability of runtime bugs. The regular wrapper class would have a noticeable performance hit during runtime due to additional heap allocations. The inline classes don't, while keeping all the benefits.

Below is a snippet of code where we should consider using a wrapper:

fun main() {
    login("qwerty", "email@qwerty.com")
}

fun login(email: String, password: String) {
    println("Please don't mix up my parameters")
}

As you can see, this code will compile nicely but would break during runtime (if it had some login logic of course).

This is where inline classes shine.

We can re-write the following code as follows:

fun main() {
    login(Email("email@qwerty.com"), Password("qwerty"))
}

fun login(email: Email, password: Password) {
    println("Please don't mix up my parameters")
}

@JvmInline
value class Email(val email: String)

@JvmInline
value class Password(val password: String)

Before Kotlin 1.5.0 we would write:

inline class Email(val email: String)

But this syntax is deprecated now and will throw a warning.

When using this approach it will be much harder to mix up our types, and during runtime no new objects will be created.

Moreover, the inline classes can have init blocks, functions, and properties (with the same restrictions as the inline properties):

@JvmInline
value class Email(val email: String) {
    init {
        require(email.isValidEmail())
    }
}

@JvmInline
value class Password(val password: String) {
    val isSecure: Boolean
        get() = password.contains('!')
}

If you want some more practical inspiration on the inline classes, you can take a look at this article by Jake Wharton.

2. Noinline modifier

At this point, we hopefully have a decent understanding of how inlining works, therefore let's take a look at the modifiers that work in the context of inline functions.

The first of those modifiers is quite straightforward.

If we don't want to (or can't) inline a lambda that is passed as a parameter to an inline function, we can mark it as noinline.

Here are a couple examples where we might wan't to mark our lambda parameter as noinline:

inline fun doubleAction(
    action1: () -> Unit,
    action2: () -> Unit
) {
    action1()

    // the following two cases won't compile
    // we cannot assign an inline lambda to a variable
    val x = action2
    // we cannot pass an inline lambda to a non-inline function
    singleAction(action2)
}

/**
 * For some reason this function is not inlined.
 * It might be too large or comes from external
 * library that we don't have any control over
 */
fun singleAction(action: () -> Unit) {
    action()
}

This can easily be fixed by telling the compiler that we don't want to inline the action2 lambda. All we need to do is mark it as noinline:

inline fun doubleAction(
    action1: () -> Unit,
    noinline action2: () -> Unit
) {
    ...
}

Now the above code will compile and work nicely.

Needless to say that if you have and inline function with one lambda as a parameter, in most cases it wouldn't make sense to mark in as noinline, and the IDE will promptly give you warning in that case.

However, it depends on what we do with that lambda parameter.

Here is an example from the Kotlin Coroutines library:

public suspend inline operator fun <T> CoroutineDispatcher.invoke(
    noinline block: suspend CoroutineScope.() -> T
): T = withContext(this, block)

The withContext is a regular function and we cannot pass an inlined lambda to it (more on why in the crossinline section):

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T

But we can still inline the withContext function where we call invoke on our CoroutineScope so this is another good example of noinline modifier in practice.

3. Crossinline modifier

To understand why we might need the crossinline modifier, we must first have a good understanding of non-local returns in Kotlin.

If you are familiar with this concept, feel free to skip the next part.

3.1. Understanding non-local returns

There are only 3 places in Kotlin where we can use a normal return without any additional labels:

  1. Named functions
    fun regularNamed(isSleepy: Boolean) {
        if (isSleepy) {
            return
        }
        // do stuff
    }
    
  2. Anonymous functions (do not confuse them with lambdas)

    fun printEven(listOfInts: List<Int>) {
        listOfInts.forEach(fun(x: Int) {
            if (x % 2 == 0) {
                println(x)
            } else {
                /** 
                * This will return at the anonymous fun
                * and continue forEach iteration.
                * Lambda would have returned at printEven
                * since forEach is inlined.
                */
                return
            }
        })
    }
    

    In most cases, we wouldn't use anonymous functions. However, keep in mind that anonymous functions allow us to specify an explicit return type, while lambdas don't.

  3. Inline functions

    fun weAllFloat(list: List<Any>) {
        list.forEach { 
            if (it !is Float) {
                /** 
                * Once again, forEach is inline
                * and will return at weAllFloat.
                * If we would like to continue looping,
                * we should use a local return@forEach
                */
                return
            } else {
                it.float() 🎈
            }
        }
    }
    

These are non-local returns.

To use a return inside a lambda, we must use either a default or a custom label:

// not the cleanest code, but illustrates our point
val hasString = lambda@{
    list.forEach { 
        if (it is String) {
            return@lambda true
        }
    }
    return@lambda false
}
print(hasString())

These are local returns.

The reason we can use a non-local return inside an inlined lambda is that it will just return from the enclosing function, where the code will be inlined.

3.2. The problem and the crossinline solution

But there is a problem that can arise with the inlined lambdas and their non-local returns:

fun main() {
    inlineFunction {
        return // this is fine, no compile error here
    }
}

inline fun inlineFunction(inlineLambda: () -> Unit) {
    randomFunction {
        inlineLambda() // compile error
    }
}

fun randomFunction(nonInlineLambda: () -> Unit) {
    nonInlineLambda()
}

The nonInlineLambda is a regular lambda and we cannot use a non-local return inside a regular lambda. Inline lambdas, however, allow non-local returns, and in this case, it would be inlined inside a regular lambda.

The compiler doesn't like that idea and since it cannot guarantee that we won't try to pass a non-local return inside an inline lambda to a regular function, it just doesn't allow passing inline lambdas to regular functions altogether.

We've had a very similar example in the noinline section, where we passed an inline lambda to the regular withContext function. In that case, the problem was solved by not inlining the lambda at all, which is one way to do it.

The other solution is the crossinline modifier. By marking a lambda parameter with crossinline we prohibit using the non-local return inside that inline lambda, and therefore can safely pass it to a regular function.

That said, let's fix our code:

fun main() {
    inlineFunction {
        // the non-local return won't compile here anymore
        return@inlineFunction
    }
}

inline fun inlineFunction(crossinline inlineLambda: () -> Unit) {
    randomFunction {
        inlineLambda() // now this is perfectly fine
    }
}

fun randomFunction(nonInlineLambda: () -> Unit) {
    nonInlineLambda()
}

Now we can have all the benefits of our inlineLambda and the compiler won't have to worry about non-local returns.

A good example of crossinline in the Kotlin standard library is the sortBy function:

public inline fun <T, R : Comparable<R>> MutableList<T>.sortBy(
    crossinline selector: (T) -> R?
): Unit

It passes the selector down to a regular function, but still inlines the comparator logic.

4. Reified modifier

Finally, we got to the final modifier that we will discuss today. This is probably my favorite one since it allows for some clever solutions.

Let's take a look at a simple example:

fun main() {
    val json = "{ ... }"
    val user = json.asObjectFromJson(User::class.java)
}

fun <T> String.asObjectFromJson(classType: Class<T>): T? {
    // validate json
    // do some deserialization
    // return an instance of classType
}

In a regular function, type T is only available during compile time and is erased at runtime. Therefore normally, we cannot infer the class of T at runtime and have to pass a Class as a parameter.

The reified modifier fixes this inconvenience. However, keep in mind that we can only use reified in the inlined functions.

Here is how we can improve our code using the reified modifier:

fun main() {
    val json = "{ ... }"
    // the call looks prettier as well
    val user = json.asObjectFromJson<User>()
}

inline fun <reified T> String.asObjectFromJson(): T? {
    // validate json
    // do some deserialization
    // we can access the class of T during runtime now!
    return T::class.java.newInstance()
}

By marking our generic T as reified we can use it inside the function as if it was a normal class.

The inline function makes this possible without using reflection, however, if you need to use it, reflection also becomes available for the reified types as well as operators like is and as that work with classes.

One of the most common examples of reified in the Kotlin standard library is the Iterable extension function filterIsInstance:

list.filterIsInstance<String>()

Conclusion

When switching from Java to Kotlin, it can be very tempting to continue writing Java code, only using Kotlin. Kotlin will not stop you from doing that.

However, Kotlin offers so much more and any Kotlin user would be doing themselves a huge disservice by missing out on a huge variety of features that Kotlin provides.

Today we have looked at some of those features, and, hopefully, this information will help you write better idiomatic Kotlin code.

See you next time.

To growing and sharing,

Your friend,

Max

Interested in reading more such articles from Max Kim?

Support the author by donating an amount of your choice.

 
Share this