Kotlin's crossinline keyword

First, I am having trouble actually picturing what is meant by "such cases". I have a general idea of what the issue is but can't come up with a good example of it.

Here's an example:

interface SomeInterface {
    fun someFunction(): Unit
}

inline fun someInterfaceBy(f: () -> Unit): SomeInterface { 
    return object : SomeInterface {
        override fun someFunction() = f() 
        //                            ^^^
        // Error: Can't inline 'f' here: it may contain non-local returns. 
        // Add 'crossinline' modifier to parameter declaration 'f'.
    }
}

Here, the function that is passed to someInterfaceBy { ... } is inlined inside an anonymous class implementing SomeInterface. Compilation of each call-site of someInterfaceBy produces a new class with a different implementation of someFunction().

To see what could go wrong, consider a call of someInterfaceBy { ... }:

fun foo() {
    val i = someInterfaceBy { return }
    // do something with `i`
}

Inside the inline lambda, return is non-local and actually means return from foo. But since the lambda is not called and leaks into the object i, return from foo may be absolutely meaningless: what if i.someFunction() (and thus the lambda) is called after foo has already returned or even in a different thread?

Generically, 'such cases' means inline functions that call their functional parameters not in their own bodies (effectively, i.e. taking other inline functions into account) but inside some other functions they declare, like in non-inline lambdas and anonymous objects.


Second, the phrase "To indicate that," can be read multiple ways. To indicate what? That a particular case is not allowed? That it is allowed? That non-local control flow in a given function definition is (or is not) allowed?

This is exactly how the problem I described above is fixed in the Kotlin language design: whenever an inline function intends to inline its functional parameter somewhere where it could be not called in-place but stored and called later, the parameter of the inline function should be marked as crossinline, indicating that non-local control flow is not allowed in the lambdas passed here.


Problem: non-local return

Let's first understand the problem of non-local return with a simple example:

fun doSomething() {
    println("Before lambda")
    doSomethingElse {
        println("Inside lambda")
        return // This is non-local return
    }
    println("After lambda")
}

inline fun doSomethingElse(lambda: () -> Unit) {
    println("Do something else")
    lambda()
}

Non-local return

In the code above, the return statement is called a non-local return because it's not local to the function in which it is called. This means this return statement is local to the doSomething() function and not to the lambda function in which it is called. So, it terminates the current function as well as the outermost function.

Local return

If you just wanted to return from the lambda, you would say return@doSomethingElse. This is called local return and it is local to the function where it is specified.

Problem

Now the problem here is that the compiler skips the lines after the non-local return statement. The decompiled bytecode for the doSomething() looks like following:

public static final void doSomething() {
    System.out.println("Before lambda");
    System.out.println("Doing something else");
    System.out.println("Inside lambda");
}

Notice that there is no statement generated for the line println("After lambda"). This is because we have the non-local return inside the lambda and the compiler thinks the code after the return statement is meaningless.


Solution: crossinline keyword

crossinline

In such cases (like the problem mentioned above), the solution is to disallow the non-local return inside the lambda. To achieve this, we mark the lambda as crossinline:

inline fun doSomethingElse(crossinline lambda: () -> Unit) {
    println("Doing something else")
    lambda()
}

Non-local return disallowed

When you use the crossinline keyword, you are telling the compiler, "give me an error, if I accidentally use a non-local return inside the nested functions or local objects.":

fun doSomething() {
    println("Before lambda")
    doSomethingElse {
        println("Inside lambda")
        return                  // Error: non-local return
        return@doSomethingElse  // OK: local return
    }
    println("After lambda")
}

Now the compiler generates the bytecode as expected:

public static final void doSomething() {
    System.out.println("Before lambda");
    System.out.println("Doing something else");
    System.out.println("Inside lambda");
    System.out.println("After lambda");
}

That's it! Hope I made it easier to understand.

Tags:

Kotlin