Kotlin Inline Functions


Before we dive into inline functions, let’s understand Closures.

Closures

Kotlin Lambdas are closures. In Kotlin, an anonymous function can reference and modify variables defined outside of its scope. This means an anonymous function has reference to the variables defined in the scope where it is itself created.

Let’s checkout an example:


In this example, variable n is defined outside the scope of lambda function but it is still accessible by lambda expressions.

The term closure is used to describe this behavior because the lambda function encloses or binds the captured variables within its own context. When we say a lambda function binds variables, it means the lambda function keeps a hold of those variables from the place where it was created and forms a self contained unit, containing the code of the lambda function and the data from the surrounding scope, that can be passed around and executed independently.

This package, called closure, can be used independently. We can pass the closure around to different parts of our program and execute it whenever needed. Even if the original scope where variables were defined has finished, the closure still has access to those variables.

Disadvantages of Higher order functions

Higher order functions that accept lambdas as parameters can introduce memory overhead. Here are a few reasons for the same:

Function objects

When you define a lambda, it is represented as an object instance on the JVM which contains the code that will be executed when the lambda is invoked. Function objects, like any other objects, require memory allocations.

Closures

As explained above, lambdas can capture references to variables from their surrounding scope. This can lead to increased memory overhead as compared to a regular function call.

Multiple instances

Higher order functions that accept lambdas as parameters often result in creation of multiple lambda instances and hence more memory allocation.

Garbage collection

If lambdas are create frequently or capture long-lived objects, it can affect the garbage collection process and potentially increase memory usage.


Inline Functions

Inline functions can help mitigate some of the memory overhead associated with higher order functions that accept lambda as parameter. The inline keyword requests the compiler to not allocate memory and simply copy the inlined code of that function at the calling place.

Let’s see how does inline functions work, using an example. In this example, I’ve created 2 functions: performOperationOnOneVar and performOperationOnTwoVars. performOperationOnTwoVars is an inline function while the former is not.

fun performOperationOnOneVar(a: Int, operation: (Int) -> Int): Unit =
    println(operation(a))

inline fun performOperationOnTwoVars(a: Int, b: Int, operation: (Int, Int) -> Int): Unit =
    println(operation(a, b))

fun square(a: Int) = a * a

fun cube(a: Int) = a * a * a

fun add(a: Int, b: Int) = a + b

fun main() {
    performOperationOnOneVar(2, ::square)
    performOperationOnOneVar(3, ::cube)
    performOperationOnTwoVars(2, 3, ::add)
}

Now, let’s see how the decompiled version of this code looks like:


In the highlighted code, you can see the compiler created a new instance for each call of lambda function from a non-inline function, whereas it copied the code of the inline function in the main function itself, hence reducing the memory overhead.

Limitations of Inline functions

In Kotlin, there are certain expressions and declarations that are not supported in inline functions. Let’s see a few in detail:

Declaration of Local classes

If local classes were supported in inline functions, the copied code in the calling function will contain the reference of the local class. However, local classes are not allowed to escape their defining scope.

Declaration of inner nested classes

If inner nested classes were supported in inline functions, the copied code could contain reference to the inner nested class. However the inner nested is tightly coupled with its containing class and requires an instance of the containing class to be instantiated. So, when the inline function is copied to the calling code, there may be an instance of the containing class available, which could result in invalid or incorrect behavior.

Default value for optional parameters

When a function is declared as inline, the body is effectively copied at the call site during compilation. This means the default values for optional parameters are also copied to the call site. If default values were allowed in inline functions, the functions will always be called with the default value even if a different value was given at runtime for the optional parameter.

Function References

When a function reference is passed as a parameter to an inline function, it would require creating a separate instance of the function reference for each call site. This would lead to increased code duplication and potentially result in larger compiled code size.

Capturing of this

When you capture this inside an inline function, the code inside the lambda has access to the state of the enclosing class or instance. This can lead to unintended behavior because the captured this reference might not be valid or might change during the execution of the inline function.



Non-local returns

In Kotlin, if we want to return from a lambda expression then the Kotlin compiler does not allow us to do so. 

But if a lambda is passed to an inline function, the return can also be inlined. Such returns (existing in lambda but exiting the enclosing functions) are called non-local returns.

In the inlined function, square(2) invoked the first expression and return keyword forced the lambda expression itself and the main function from where it is called to exit.



Crossinline annotation

In the above program, return in lambda exits the inline function as well as its enclosing function. So to stop returning from the lambda expression enclosing inline function, we can annotate the inline function as crossinline. This will throw a compile-time error if it finds a return statement in lambda expression.



Noinline annotation

In inline functions if we want only some of the lambdas to be inline, we can annotate the others with the noinline keyword.

Here the return statement in square lambda does not give a error but the return statement in the cube lambda gives an error because it was marked as noinline.



Reified parameters

Sometimes we want to access the type of parameter passed during the call. We have to simply pass the parameter at the time of function calling and we can retrieve the type of the parameter using a reified modifier.

Only type parameters of inline functions can be reified


Inline properties

The inline modifier can be used on accessors of properties that don’t have backing fields. You can annotate individual property accessors:

val client: Client
    inline get() = Client()

var employee: Employee
    get() = ...
    inline set(v) { ... }

You can also annotate an entire property, which marks both of its accessors as inline:

inline var employee: Employee
    get() = ...
    set(v) { ... }

At the call site, inline accessors are inlined as regular inline functions.


Restrictions for public API inline functions

When an inline function is public or protected but is not a part of a private or internal declaration, it is considered a module’s public API. It can be called in other modules and is inlined at such call sites as well.

This imposes certain risks of binary incompatibility caused by changes in the module that declares an inline function in case the calling module is not re-compiled after the change.

To eliminate the risk of such incompatibility being introduced by a change in a non-public API of a module, public API inline functions are not allowed to use non-public-API declarations, i.e. private and internal declarations and their parts, in their bodies.

An internal declaration can be annotated with @PublishedApi, which allows its use in public API inline functions. When an internal inline function is marked as @PublishedApi, its body is checked too, as if it were public.



That’s it for this article. Hope it was helpful! If you like it, please hit like.

Other articles of this series:


Leave a comment